はじめに
こんにちは、エムティーアイ Advent Calendar 2024 の12月18日分の執筆を担当させていただく新卒エンジニアの小林です。
今回は、前回の【Flutter】BottomNavigationBarの状態管理方法3選の番外編として、GoRouterを使ってBottomNavigationBarの画面切り替えを行う方法についてご紹介します。
GoRouter導入済みのFlutterプロジェクトでBottomNavigationBarを追加する際、GoRouterによってBottomNavigationBarによる画面切り替えを実現する方法について検証を行なった内容についてまとめました。
FlutterプロジェクトにGoRouterを導入済み、または導入予定で、GoRouterを使ったBottomNavigationBarの画面切り替えを行う方法を知りたいという方におすすめです。
GoRouterとは
既にご存知の方も多いかもしれませんが、GoRouterについて軽くご紹介しておきます。
GoRouterとは、Flutterの画面遷移を行うためのルーティングpackageです。
GoRouterは、URLベースで画面遷移を行うことができるため、標準の画面遷移で使われるNavigatorと比べてシンプルなコードで画面遷移を実現することができます。
GoRouterのインストール
まず、GoRouterの導入を行います。
以下のようにpubspec.yaml
にgo_router: ^14.6.2
(記事執筆時点の最新バージョン)を追加し、flutter pub get
を実行してください。
dependencies: go_router: ^14.6.2
これで、GoRouterのインストールは完了です。
表示する画面の作成
次に、BottomNavigationBarで表示切り替えを行う画面を作成します。
BottomNavigationBarの公式サンプルコードをベースに、HomePage
、BusinessPage
、SchoolPage
の3つの画面を作成します。
const TextStyle optionStyle = TextStyle(fontSize: 30, fontWeight: FontWeight.bold); class HomePage extends StatelessWidget { const HomePage({super.key}); @override Widget build(BuildContext context) { return const Scaffold( body: Center( child: Text( 'Index 0: Home', style: optionStyle, ), ), ); } } // 省略(以下HomePageと同様にBusinessPageとSchoolPageを追加で作成)
これで、ButtomNavigationBarによる切り替えを行うためのシンプルな画面ができました。
本記事では、説明のために1つのDartファイルにまとめて記述していますが、実際のプロジェクトにおいてはそれぞれのクラスをファイルに分割したり、スタイルを共通化したりすることをおすすめします。
BottomNavigationBarの作成
表示する画面ができたので、その画面を切り替えるためのBottomNavigationBarを作成します。
以下のようなLayoutScaffold
クラスを作成します。
class LayoutScaffold extends StatelessWidget { const LayoutScaffold({ super.key, required this.navigationShell, }); final StatefulNavigationShell navigationShell; @override Widget build(BuildContext context) { return Scaffold( body: navigationShell, 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: navigationShell.currentIndex, // 選択中のindex selectedItemColor: Colors.amber[800], onTap: (index) => navigationShell.goBranch(index), // indexの切り替え ), ); } }
LayoutScaffold
クラスは、StatefulNavigationShell型の引数navigationShell
を受け取ります。
これはGoRouterが提供する機能で、StatefulNavigationShell型の値には、BottomNavigationBarによる画面切り替えを行うために必要な情報が含まれています。
navigationShell
には、現在選択中の画面の情報が含まれています。
そのため、Scaffoldのbody
にはnavigationShell
をそのまま渡します。
BottomNavigationBarのcurrentIndex
には、選択中のindexを表すnavigationShell.currentIndex
を指定します。
BottomNavigationBarのonTap
には、indexの切り替えを行うnavigationShell.goBranch(index)
を指定します。
通常、BottomNavigationBarの画面切り替えは状態管理を用いて行うのが一般的ですが、StatefulNavigationShellを使うと、GoRouterがで内部的に状態管理を行ってくれるため、StatelessWidgetでBottomNavigationBarの画面切り替えを行うことができます。
面白いですね。
これでBottomNavigationBarが作成できました。
ルーティングの設定
続いて、ルーティングの設定を行います。
GoRouterを用いて以下のようにルーティングの設定を記述します。
final router = GoRouter( initialLocation: '/home', routes: [ // 複数の画面の状態を保持しつつ画面の切り替えを行う StatefulShellRoute.indexedStack( // 各画面共通のレイアウト builder: (context, state, navigationShell) => LayoutScaffold( navigationShell: navigationShell, ), branches: [ // HomePageのルーティング設定 StatefulShellBranch( routes: [ GoRoute( path: '/home', pageBuilder: (context, state) => MaterialPage( key: state.pageKey, child: const HomePage(), ), ), ], ), // BusinessPageのルーティング設定 StatefulShellBranch( routes: [ GoRoute( path: '/business', pageBuilder: (context, state) => MaterialPage( key: state.pageKey, child: const BusinessPage(), ), ), ], ), // SchoolPageのルーティング設定 StatefulShellBranch( routes: [ GoRoute( path: '/school', pageBuilder: (context, state) => MaterialPage( key: state.pageKey, child: const SchoolPage(), ), ), ], ), ], ), ], );
StatefulShellRoute.indexedStack
とStatefulShellBranch
はGoRouterが提供する機能であり、GoRouterによるBottomNavigationBarの切り替えに必要となります。
StatefulShellRoute.indexedStack
は、複数の画面の状態を保持しつつ、画面の切り替えを行うための機能です。
builder
には、各画面共通のレイアウトを指定することができるため、先ほど作成したLayoutScaffold
を指定することにより、BottomNavigationBarは各画面共通で表示しつつ、画面のみ切り替えることができます。
StatefulShellBranchはGoRouter
は、状態を保持したまま切り替えたい画面を指定するための機能です。
router
が作成できたら、MaterialAppを以下のように編集します。
// MaterialAppではなくMaterialApp.routerを使う return MaterialApp.router( // 作成したrouterを指定 routerConfig: router, title: 'Flutter Demo', theme: ThemeData( colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple), useMaterial3: true, ), );
MaterialApp
をMaterialApp.router
に変更し、routerConfig
に先ほど作成したrouter
を指定します。
これで、作成したルーティング情報をアプリに反映させることができます。
動作確認
最後に、動作確認を行います。
ここまでできたら、BottomNavigationBarによる画面の切り替えができているか実際にアプリを起動して確かめてみましょう。
上手く動作しました!
サンプルコードの全文も載せておきます。
import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; void main() { runApp(const MyApp()); } final router = GoRouter( initialLocation: '/home', routes: [ StatefulShellRoute.indexedStack( builder: (context, state, navigationShell) => LayoutScaffold( navigationShell: navigationShell, ), branches: [ StatefulShellBranch( routes: [ GoRoute( path: '/home', pageBuilder: (context, state) => MaterialPage( key: state.pageKey, child: const HomePage(), ), ), ], ), StatefulShellBranch( routes: [ GoRoute( path: '/business', pageBuilder: (context, state) => MaterialPage( key: state.pageKey, child: const BusinessPage(), ), ), ], ), StatefulShellBranch( routes: [ GoRoute( path: '/school', pageBuilder: (context, state) => MaterialPage( key: state.pageKey, child: const SchoolPage(), ), ), ], ), ], ), ], ); class MyApp extends StatelessWidget { const MyApp({super.key}); @override Widget build(BuildContext context) { return MaterialApp.router( routerConfig: router, title: 'Flutter Demo', theme: ThemeData( colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple), useMaterial3: true, ), ); } } const TextStyle optionStyle = TextStyle(fontSize: 30, fontWeight: FontWeight.bold); class HomePage extends StatelessWidget { const HomePage({super.key}); @override Widget build(BuildContext context) { return const Scaffold( body: Center( child: Text( 'Index 0: Home', style: optionStyle, ), ), ); } } class BusinessPage extends StatelessWidget { const BusinessPage({super.key}); @override Widget build(BuildContext context) { return const Scaffold( body: Center( child: Text( 'Index 1: Business', style: optionStyle, ), ), ); } } class SchoolPage extends StatelessWidget { const SchoolPage({super.key}); @override Widget build(BuildContext context) { return const Scaffold( body: Center( child: Text( 'Index 2: School', style: optionStyle, ), ), ); } } class LayoutScaffold extends StatelessWidget { const LayoutScaffold({ super.key, required this.navigationShell, }); final StatefulNavigationShell navigationShell; @override Widget build(BuildContext context) { return Scaffold( body: navigationShell, 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: navigationShell.currentIndex, selectedItemColor: Colors.amber[800], onTap: (index) => navigationShell.goBranch(index), ), ); } }
まとめ
GoRouterを使ってBottomNavigationBarの画面切り替えを行う方法についてご紹介いたしました。
GoRouterが提供するStatefulShellRoute.indexedStack
とStatefulShellBranch
を使うと、BottomNavigationBarの画面切り替えを実現できることがわかりました。
その際、GoRouterが内部的に画面切り替えに関する状態管理を行い、BottomNavigationBarをStatelessWidgetで実現できるため、BottomNavigationBarのためにStatefulWidgetやRiverpod等の状態管理を行わなくても済むということもわかりました。
この方法を用いることにより、画面遷移だけでなく、BottomNavigationBarの画面切り替えもGoRouterに統一することができます。
ぜひ、試してみてください。
ここまで読んでくださりありがとうございました。