はじめに
こんにちは、エムティーアイ Advent Calendar 2024 12月10日分の執筆を担当させていただく新卒エンジニアの小林です。
今回は、FlutterのBottomNavigationBarの状態管理方法について3種類の方法を紹介します。
公式ドキュメントのBottomNavigationBarをベースに、それぞれの状態管理方法のサンプルコードを載せているため、BottomNavigationBarの状態管理方法のカタログ集のように使っていただけますと幸いです。
BottomNavigationBarとは何か
BottomNavigationBarとは、アプリの画面下部に配置されるナビゲーションバーのことです。 アイテムをタップすることで、アプリ内の異なるページに遷移することができます。 恐らく、ほとんどの方がアプリを触っていて、一度は使ったことがあるUI要素なのではないでしょうか。
1. StatefulWidgetを使った状態管理
まずは、一番オーソドックスなStatefulWidgetを使った方法について紹介します。 BottomNavigationBarの公式ドキュメントでは、StatefulWidgetを使った方法が紹介されています。 以下は、公式ドキュメントから引用したStatefulWidgetを使ったBottomNavigationBarのサンプルコードです。
import 'package:flutter/material.dart'; /// Flutter code sample for [BottomNavigationBar]. void main() => runApp(const BottomNavigationBarExampleApp()); class BottomNavigationBarExampleApp extends StatelessWidget { const BottomNavigationBarExampleApp({super.key}); @override Widget build(BuildContext context) { return const MaterialApp( home: BottomNavigationBarExample(), ); } } class BottomNavigationBarExample extends StatefulWidget { const BottomNavigationBarExample({super.key}); @override State<BottomNavigationBarExample> createState() => _BottomNavigationBarExampleState(); } class _BottomNavigationBarExampleState extends State<BottomNavigationBarExample> { int _selectedIndex = 0; static const TextStyle optionStyle = TextStyle(fontSize: 30, fontWeight: FontWeight.bold); static const List<Widget> _widgetOptions = <Widget>[ Text( 'Index 0: Home', style: optionStyle, ), Text( 'Index 1: Business', style: optionStyle, ), Text( 'Index 2: School', style: optionStyle, ), ]; void _onItemTapped(int index) { setState(() { _selectedIndex = index; }); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text('BottomNavigationBar Sample'), ), body: Center( child: _widgetOptions.elementAt(_selectedIndex), ), bottomNavigationBar: BottomNavigationBar( items: const <BottomNavigationBarItem>[ BottomNavigationBarItem( icon: Icon(Icons.home), label: 'Home', ), BottomNavigationBarItem( icon: Icon(Icons.business), label: 'Business', ), BottomNavigationBarItem( icon: Icon(Icons.school), label: 'School', ), ], currentIndex: _selectedIndex, selectedItemColor: Colors.amber[800], onTap: _onItemTapped, ), ); } }
引用元: BottomNavigationBar class - material library - Dart API
Home, Business, Schoolの3つの画面のListである_widgetOptions
を作成し、_selectedIndex
の値に応じてページを切り替えています。
BottomNavigationBarItemがタップされた際、_onItemTapped
が実行され、_selectedIndex
の値が変更されます。
StatefulWidgetなので、状態の更新は必ずsetStateメソッドを使って行う必要があります。
2. Hooksを使った状態管理
次に、Hooksを使った状態管理方法について紹介します。 Hooksは、Flutterで状態管理を行うためのpackageで、Hooksを使うとStatefulWidgetと比べて簡潔に状態管理を行うことができます。
Hooksを使ったコードを書く前に、以下のコマンドを実行してHooksを使用するのに必要なパッケージをインストールします。
$ flutter pub add flutter_hooks
Hooksの準備ができたら、以下のコードを書きます。
import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; void main() => runApp(const BottomNavigationBarExampleApp()); class BottomNavigationBarExampleApp extends StatelessWidget { const BottomNavigationBarExampleApp({super.key}); @override Widget build(BuildContext context) { return const MaterialApp( home: BottomNavigationBarExample(), ); } } class BottomNavigationBarExample extends HookWidget { const BottomNavigationBarExample({super.key}); static const TextStyle optionStyle = TextStyle(fontSize: 30, fontWeight: FontWeight.bold); static const List<Widget> _widgetOptions = <Widget>[ Text( 'Index 0: Home', style: optionStyle, ), Text( 'Index 1: Business', style: optionStyle, ), Text( 'Index 2: School', style: optionStyle, ), ]; @override Widget build(BuildContext context) { // useStateを使ってselectedIndexを管理 // 初期値を0に設定 final selectedIndex = useState(0); // onItemTappedでselectedIndexを更新 void onItemTapped(int index) { selectedIndex.value = index; } return Scaffold( appBar: AppBar( title: const Text('BottomNavigationBar Sample'), ), body: Center( child: _widgetOptions.elementAt(selectedIndex.value), ), bottomNavigationBar: BottomNavigationBar( items: const <BottomNavigationBarItem>[ BottomNavigationBarItem( icon: Icon(Icons.home), label: 'Home', ), BottomNavigationBarItem( icon: Icon(Icons.business), label: 'Business', ), BottomNavigationBarItem( icon: Icon(Icons.school), label: 'School', ), ], currentIndex: selectedIndex.value, selectedItemColor: Colors.amber[800], onTap: onItemTapped, ), ); } }
状態管理の方法は異なりますが、_widgetOptions
をindexの値に応じて切り替えるという考え方自体は先ほどと同じです。
先ほどはBottomNavigationBarExample
クラスがStatefulWidgetを継承していましたが、Hooksで状態管理を行う場合はHookWidgetを継承させます。
selectedIndex
やonItemTapped
はHooksWidgetを継承したクラスのbuildメソッド内に定義します。
BottomNavigationBarItemがタップされた際、onItemTapped
が実行され、selectedIndex
の値が変更されます。
Hooksを使うと、StatefulWidgetと異なりsetState
が不要になるため、状態の更新の処理をより簡潔に書くことができましたね。
3. Riverpodを使った状態管理
最後に、Riverpodを使った方法について紹介します。 RiverpodもFlutterで状態管理を行うためのpackageで、UIと状態管理ロジックを分割して記述できるなどの利点があります。
Riverpodを使ったコードを書く前に、以下のコマンドを実行してRiverpodを使用するのに必要なパッケージをインストールします。
$ flutter pub add flutter_riverpod $ flutter pub add riverpod_annotation $ flutter pub add dev:riverpod_generator $ flutter pub add dev:build_runner
Riverpodで状態管理を行う際、ProviderScope
を使ってプロジェクトのルートWidget(今回だとBottomNavigationBarExampleAppが該当
)を必ずラップしてください。
void main() { return runApp(const ProviderScope(child: BottomNavigationBarExampleApp())); }
Riverpodの導入準備ができたら、状態の更新処理を行うためのNotifierを作成します。 以下のコードを書きます。
import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; // Riverpod Generatorを用いるためにファイル名に.g.dartを付けたpartを指定 part 'main.g.dart'; // (省略) @riverpod class SelectedIndexNotifier extends _$SelectedIndexNotifier { @override int build() { // buildの型をintで指定すると、int型のstateを持つNotifierが生成される // returnで初期値を0に指定 return 0; } // onItemTappedでstateを更新 void onItemTapped(int index) { state = index; } } // (省略)
@riverpod
アノテーションを付け、_$SelectedIndexNotifier
を継承したSelectedIndexNotifier
クラスを作成します。
Notifierのbuildメソッドをint型に指定することで、int型のstateを持つNotifierが生成されます。
buildメソッドのreturn文には、stateの初期値を指定することができるため、0
を指定します。
onItemTapped
メソッドには、state(今回だとbuildで指定した通りint型)の値の更新を行う処理を書きます。
Riverpod Generatorを用いたコード生成を行うため、必ずNotifierを定義したファイルにpart '{ファイル名}.g.dart';
の記述を行い、以下のコマンドを忘れないように実行してください。
$ dart run build_runner watch
これでロジック部分を記述したNotifierは完成です。 続いて、UI部分のコードを以下のように書きます。
// (省略) class BottomNavigationBarExample extends ConsumerWidget { const BottomNavigationBarExample({super.key}); static const TextStyle optionStyle = TextStyle(fontSize: 30, fontWeight: FontWeight.bold); static const List<Widget> _widgetOptions = <Widget>[ Text( 'Index 0: Home', style: optionStyle, ), Text( 'Index 1: Business', style: optionStyle, ), Text( 'Index 2: School', style: optionStyle, ), ]; @override Widget build(BuildContext context, WidgetRef ref) { // ref.watchでselectedIndexNotifierProviderを監視し、selectedIndexを取得 final selectedIndex = ref.watch(selectedIndexNotifierProvider); // ref.readでselectedIndexNotifierProvider.notifierを取得 final notifier = ref.read(selectedIndexNotifierProvider.notifier); return Scaffold( appBar: AppBar( title: const Text('BottomNavigationBar Sample'), ), body: Center( child: _widgetOptions.elementAt(selectedIndex), ), bottomNavigationBar: BottomNavigationBar( items: const <BottomNavigationBarItem>[ BottomNavigationBarItem( icon: Icon(Icons.home), label: 'Home', ), BottomNavigationBarItem( icon: Icon(Icons.business), label: 'Business', ), BottomNavigationBarItem( icon: Icon(Icons.school), label: 'School', ), ], currentIndex: selectedIndex, selectedItemColor: Colors.amber[800], onTap: notifier.onItemTapped, ), ); } }
状態管理の方法は異なりますが、_widgetOptions
をindexの値に応じて切り替えるという考え方自体は先ほどと同じです。
Riverpodで状態管理を行うため、今度はBottomNavigationBarExample
クラスにConsumerWidgetを継承させます。
ConsumerWidgetを継承したクラスのbuildメソッドには、追加でWidgetRef型の引数refが必要になるため、忘れないように追加してください。
ここまでできたら、buildメソッド内でselectedIndex
とnotifier
を定義します。
ref.watch
でselectedIndexNotifierProvider
のstate(今回だとint型の値)を取得できるため、selectedIndex
に代入します。
ref.read
でselectedIndexNotifierProvider.notifier
を取得し、notifier
に代入します。ConsumerWidgetを継承したクラス内でnotifier.onItemTapped
のようにNotifierで定義したメソッドが使用することができます。
BottomNavigationBarItemがタップされた際、notifier.onItemTapped
が実行され、selectedIndex
の値が変更されます。
以下がRiverpodを使ったBottomNavigationBarのコードの全文です。
import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; // Riverpod Generatorを用いるためにファイル名に.g.dartを付けたpartを指定 part 'main.g.dart'; void main() { return runApp(const ProviderScope(child: BottomNavigationBarExampleApp())); } class BottomNavigationBarExampleApp extends StatelessWidget { const BottomNavigationBarExampleApp({super.key}); @override Widget build(BuildContext context) { return const MaterialApp( home: BottomNavigationBarExample(), ); } } @riverpod class SelectedIndexNotifier extends _$SelectedIndexNotifier { @override int build() { // buildの型をintで指定すると、int型のstateを持つNotifierが生成される // returnで初期値を0に指定 return 0; } // onItemTappedでstateを更新 void onItemTapped(int index) { state = index; } } class BottomNavigationBarExample extends ConsumerWidget { const BottomNavigationBarExample({super.key}); static const TextStyle optionStyle = TextStyle(fontSize: 30, fontWeight: FontWeight.bold); static const List<Widget> _widgetOptions = <Widget>[ Text( 'Index 0: Home', style: optionStyle, ), Text( 'Index 1: Business', style: optionStyle, ), Text( 'Index 2: School', style: optionStyle, ), ]; @override Widget build(BuildContext context, WidgetRef ref) { // ref.watchでselectedIndexNotifierProviderを監視し、selectedIndexを取得 final selectedIndex = ref.watch(selectedIndexNotifierProvider); // ref.readでselectedIndexNotifierProvider.notifierを取得 final notifier = ref.read(selectedIndexNotifierProvider.notifier); return Scaffold( appBar: AppBar( title: const Text('BottomNavigationBar Sample'), ), body: Center( child: _widgetOptions.elementAt(selectedIndex), ), bottomNavigationBar: BottomNavigationBar( items: const <BottomNavigationBarItem>[ BottomNavigationBarItem( icon: Icon(Icons.home), label: 'Home', ), BottomNavigationBarItem( icon: Icon(Icons.business), label: 'Business', ), BottomNavigationBarItem( icon: Icon(Icons.school), label: 'School', ), ], currentIndex: selectedIndex, selectedItemColor: Colors.amber[800], onTap: notifier.onItemTapped, ), ); } }
Riverpodを使うと、コード量は増えるものの、UIと状態管理ロジックを分割できましたね。
最後に
FlutterのBottomNavigationBarの状態管理方法について、StatefulWidget、Hooks、Riverpodの3つの方法を紹介しました。
これらの方法を全て丸暗記する必要ありません。
大事なのは、各々のプロジェクトや個人の技術スタックに応じて柔軟に使い分けることだと思います。