【Flutter】RiverpodでBottomNavigationBar

25226 ワード

はじめに

こんにちは。にしやまです。

最近プライベートではFlutterを触ってみています。
同じようなことをしている記事は他にもいくつかあるのですが、少しハマったところがあったので記事にしようと思います。
BottomNavigationBarとは、アプリ画面下部のタブを作る部品です。

なお、Riverpodについて詳しく知りたい方は公式ドキュメントをご覧ください。
日本語にも切り替えられて英語の読めない愚かな僕にも親切で嬉しいです。(英語勉強しなきゃ...)

実際の動作

動作させている様子がこちらになります。
(ちょっと画質荒いですが...)
base_tab_view.gif

環境

$ flutter --version
Flutter 2.10.4 • channel stable • https://github.com/flutter/flutter.git
Framework • revision c860cba910 (6 weeks ago)2022-03-25 00:23:12 -0500
Engine • revision 57d3bac3dd
Tools • Dart 2.16.2 • DevTools 2.9.2

pubspec.yaml

flutter_riverpod: ^1.0.3

コード

大したことはしていませんがコードを載せた後で簡単に解説します。

main.dart

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:sample/views/base_tab_view.dart';

void main() {
  // ProviderScopeで囲むことでriverpodを利用
  runApp(const ProviderScope(child: MyApp()));
}

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'test',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      // BottomNavigationBarを実装したアプリのbaseとなるviewを利用
      home: BaseTabView(),
    );
  }
}

main.dartはコメント以上の解説は不要かと思いますので省きます。

views/home_view.dart

import 'package:flutter/material.dart';

class HomeView extends StatelessWidget {
  const HomeView({Key? key}) : super(key: key);

  
  Widget build(BuildContext context) {
    return const Scaffold(
      body: Center(
        child: Icon(
          Icons.home,
        ),
      ),
    );
  }
}

こちらはサンプルの為の簡単な画面です。
アイコンとクラス名とファイル名だけ変えたものをhome, info, helpと用意しました。

views/base_tab_view.dart

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:sample/views/help_view.dart';
import 'package:sample/views/home_view.dart';
import 'package:sample/views/info_view.dart';

final baseTabViewProvider = StateProvider<ViewType>((ref) => ViewType.home);

enum ViewType { home, info, help }

class BaseTabView extends ConsumerWidget {
  BaseTabView({Key? key}) : super(key: key);

  final widgets = [
    const HomeView(),
    const InfoView(),
    const HelpView(),
  ];

  
  Widget build(BuildContext context, WidgetRef ref) {
    final view = ref.watch(baseTabViewProvider.state);
    return Scaffold(
      appBar: AppBar(title: Text(view.state.name)),
      body: widgets[view.state.index],
      bottomNavigationBar: BottomNavigationBar(
        items: const [
          BottomNavigationBarItem(icon: Icon(Icons.home), label: 'home'),
          BottomNavigationBarItem(icon: Icon(Icons.info), label: 'info'),
          BottomNavigationBarItem(icon: Icon(Icons.help), label: 'help'),
        ],
        currentIndex: view.state.index,
        onTap: (int index) => view.update((state) => ViewType.values[index]),
        type: BottomNavigationBarType.fixed,
      ),
    );
  }
}

解説

base_tab_view.dartについて解説します。

final baseTabViewProvider = StateProvider<ViewType>((ref) => ViewType.home);

→ StateProviderはシンプルなステート(状態)を管理する際に使われます。
RiverpodのドキュメントにはStateProviderを使うべきではない目安として以下のように記載されています。

逆に言えば、StateProvider は次のようなステートを公開するために使うべきではありません。

  • ステートの算出に何かしらのバリデーション(検証)ロジックが必要
  • ステート自体が複雑なオブジェクトである(カスタムのクラスや List/Map など)
  • ステートを変更するためのロジックが単純な count++ よりは高度である必要がある

今回はどれも満たさないごく単純なステート管理なのでStateProviderを用いています。

StateProviderの宣言時にViewTypehomeを返していることにはいくつか理由があります。

  • タブの初期位置はhome固定の為
  • タブのリストはenumで管理しなくても切り替え可能だが、種類を明示的にする為
class BaseTabView extends ConsumerWidget {

→ flutter_riverpodとして扱う為にはConsumerWidgetをextendsします。
ConsumerWidgetはWidget build(BuildContext context, WidgetRef ref)としてWidgetRefを取得できること以外はほとんどStatelessWidgetと同じです。

final widgets = [
  const HomeView(),
  const InfoView(),
  const HelpView(),
];

→ bodyに渡す実際に表示させたいWidgetをListで定義します。

final view = ref.watch(baseTabViewProvider.state);

→ ここで"state"を渡してあげないとviewがbaseTabViewProviderの変更をlistenしてくれない為タブがタップされても画面が遷移しない現象が発生します。
ref.watch(baseTabViewProvider.notifier);と書いてしまって画面遷移せずしばらくハマってしまいました)

  • ref.watch(baseTabViewProvider.state): StateNotifierを取得しステートをlistenする
  • ref.watch(baseTabViewProvider.notifier): StateNotifierを取得するだけでステートのlistenはしない
body: widgets[view.state.index],

→ その時選択されているステートによって渡されるWidgetが変わります。
タブがタップされステートの値が変わる度に画面が再描画されます。

bottomNavigationBar: BottomNavigationBar(
  items: const [
    BottomNavigationBarItem(icon: Icon(Icons.home), label: 'home'),
    BottomNavigationBarItem(icon: Icon(Icons.info), label: 'info'),
    BottomNavigationBarItem(icon: Icon(Icons.help), label: 'help'),
  ],
  currentIndex: view.state.index,
  onTap: (int index) => view.update((state) => ViewType.values[index]),
  type: BottomNavigationBarType.fixed,
),

onTapのところで先ほどref.watch()で取得したStateProviderの値を更新しています。

おわり

以上です。
これでビルドすると上の添付のようになるはずです。

参考