【Flutter】go_router / go_router_builderとBottomNavigationBarの構築ガイド
はじめに
この記事は go_router/go_router_builder を使って BottomNavigationBar と連携する方法を段階的に試していった記事になります
go_router
異なる画面間を移動するための便利な URL ベースの API を提供します。go_router を使用すると、Flutter の画面遷移やルーティング管理を非常にシンプルなコードで実装できます
go_router_builder
go_router_builder | Dart Package
Flutter の宣言型ルーティングパッケージである go_router 用のbuilderです。go_router_builder を使用すると、生成された強力に型指定されたルートヘルパーをサポートできます
環境構築や準備
各バージョンや環境
$ fvm --version
2.4.1
$ fvm flutter --version
Flutter 3.16.5 • channel stable • https://github.com/flutter/flutter.git
Framework • revision 78666c8dc5 (5 days ago) • 2023-12-19 16:14:14 -0800
Engine • revision 3f3e560236
Tools • Dart 3.2.3 • DevTools 2.28.4
$ sw_vers
ProductName: macOS
ProductVersion: 13.4.1
ProductVersionExtra: (c)
BuildVersion: 22F770820d
1. プロジェクトの作成
今回は go_router_example
というプロジェクト名で進めていきます。
mkdir go_router_example
cd go_router_example
fvm use 3.16.5 --force
fvm flutter create .
2. pubの追加
dependencies:
go_router: ^13.0.0
dev_dependencies:
build_runner: ^2.4.7
go_router_builder: ^2.4.0
上記を追加して fvm flutter pub get
を実行します。
実装
1. Homeルートを実装
lib/hoge_page.dart
の作成
1-1. 事前に HomePage
を作成しておきます。中身はHomePageが表示されている単純なものになります。
import 'package:flutter/material.dart';
class HomePage extends StatelessWidget {
const HomePage({super.key});
Widget build(BuildContext context) {
return const Scaffold(
body: Center(
child: Text('Home Page'),
),
);
}
}
lib/router/router.dart
の作成
1-2. 以下の内容で router/router.dart
を作成します。 またHomePage
のimportも追加しときます。
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:go_router_example/hoge_page.dart';
part 'router.g.dart';
final GlobalKey<NavigatorState> rootNavigatorKey = GlobalKey<NavigatorState>();
final router = GoRouter(
routes: $appRoutes,
initialLocation: HomeRoute.path,
navigatorKey: rootNavigatorKey,
);
<HomeRoute>(
path: HomeRoute.path,
)
class HomeRoute extends GoRouteData {
const HomeRoute();
static const path = '/home';
Widget build(BuildContext context, GoRouterState state) => const HomePage();
}
以下コマンドを実施し router.g.dart
を生成します。
fvm flutter pub run build_runner build --delete-conflicting-outputs
この時点で生成された `router.g.dart`
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'router.dart';
// **************************************************************************
// GoRouterGenerator
// **************************************************************************
List<RouteBase> get $appRoutes => [
$homeRoute,
];
RouteBase get $homeRoute => GoRouteData.$route(
path: '/home',
factory: $HomeRouteExtension._fromState,
);
extension $HomeRouteExtension on HomeRoute {
static HomeRoute _fromState(GoRouterState state) => const HomeRoute();
String get location => GoRouteData.$location(
'/home',
);
void go(BuildContext context) => context.go(location);
Future<T?> push<T>(BuildContext context) => context.push<T>(location);
void pushReplacement(BuildContext context) =>
context.pushReplacement(location);
void replace(BuildContext context) => context.replace(location);
}
lib/app.dart
と lib/main.dart
の作成
1-2. 最後に app.dart
と main.dart
を以下内容で作成します。
-
app.dart
lib/app.dartimport 'package:flutter/material.dart'; import 'package:go_router_example/router/router.dart'; class App extends StatelessWidget { const App({super.key}); Widget build(BuildContext context) { return MaterialApp.router( title: 'Router Demo', theme: ThemeData( colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple), useMaterial3: true, ), routerConfig: router, ); } }
-
main.dart
lib/main.dartimport 'package:flutter/material.dart'; import 'package:go_router_example/app.dart'; void main() { runApp(const App()); }
実行してみて HomePage
が表示されとけばOKです。
2. BottomNavigationBarと連携 (状態が残らないパターン)
次に先ほど作成した HomePage
と新たに作成する SettingsPage
の2ページを切り替えれるBottomNavigationBarを持つページを作成してみたいと思います。
但し HomePage
で操作して SettingsPage
へ切り替えても HomePage
で操作した内容は残らずクリアされてしまうパターンとなります。後述するクリアされないパターンの比較としてこちらも試していきます。
BottomNavigationBarを持つページとして TopPage
を作成する前提で router/router.dart
を修正します。
lib/settings_page.dart
と lib/top_page.dart
を作成
2-1. -
settings_page.dart
lib/settings_page.dartimport 'package:flutter/material.dart'; class SettingsPage extends StatelessWidget { const SettingsPage({super.key}); Widget build(BuildContext context) { return const Scaffold( body: Center( child: Text('Settings Page'), ), ); } }
-
top_page.dart
lib/top_page.dartimport 'package:flutter/material.dart'; import 'package:go_router_example/router/router.dart'; enum PageIndex { home, settings } class TopPage extends StatefulWidget { const TopPage({required this.child, super.key}); final Widget child; State<TopPage> createState() => _TopPageState(); } class _TopPageState extends State<TopPage> { int _selectedIndex = 0; Widget build(BuildContext context) { return Scaffold( body: widget.child, bottomNavigationBar: _bottomNavigationBar(context), ); } BottomNavigationBar _bottomNavigationBar(BuildContext context) { return BottomNavigationBar( currentIndex: _selectedIndex, items: const <BottomNavigationBarItem>[ BottomNavigationBarItem( icon: Icon(Icons.home), label: "Home", ), BottomNavigationBarItem( icon: Icon(Icons.settings), label: "Settings", ), ], onTap: (int index) { setState(() { _selectedIndex = index; }); if (index == PageIndex.home.index) { const HomeRoute().go(context); } else if (index == PageIndex.settings.index) { const SettingsRoute().go(context); } }, ); } }
lib/router/router.dart
に SettingsPage
用の SettingsRoute
追加
2-2. <SettingsRoute>(
path: SettingsRoute.path,
)
class SettingsRoute extends GoRouteData {
const SettingsRoute();
static const path = '/settings';
Widget build(BuildContext context, GoRouterState state) => const SettingsPage();
}
lib/router/router.dart
に TopShellRoute
を追加
2-3. -
TopPage
用のShellRouteDataになります- 子のrouteとして
HomeRoute
とSettingsRoute
を指定しています -
TopShellRoute
用に NavigatorState を設定しています-
$navigatorKey
というstaticなクラス変数に設定します
-
- 子のrouteとして
final GlobalKey<NavigatorState> rootNavigatorKey = GlobalKey<NavigatorState>();
// ↓新規に追加
final GlobalKey<NavigatorState> shellNavigatorKey = GlobalKey<NavigatorState>();
// ....
<TopShellRoute>(
routes: <TypedRoute<RouteData>>[
TypedGoRoute<HomeRoute>(path: HomeRoute.path),
TypedGoRoute<SettingsRoute>(path: SettingsRoute.path),
],
)
class TopShellRoute extends ShellRouteData {
const TopShellRoute();
static final GlobalKey<NavigatorState> $navigatorKey = shellNavigatorKey;
Widget builder(
BuildContext context,
GoRouterState state,
Widget navigator,
) {
return TopPage(child: navigator);
}
}
ここまでの `router.dart` 全体はこちら
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:go_router_example/hoge_page.dart';
import 'package:go_router_example/settings_page.dart';
import 'package:go_router_example/top_page.dart';
part 'router.g.dart';
final GlobalKey<NavigatorState> rootNavigatorKey = GlobalKey<NavigatorState>();
final GlobalKey<NavigatorState> shellNavigatorKey = GlobalKey<NavigatorState>();
final router = GoRouter(
routes: $appRoutes,
initialLocation: HomeRoute.path,
navigatorKey: rootNavigatorKey,
);
<TopShellRoute>(
routes: <TypedRoute<RouteData>>[
TypedGoRoute<HomeRoute>(path: HomeRoute.path),
TypedGoRoute<SettingsRoute>(path: SettingsRoute.path),
],
)
class TopShellRoute extends ShellRouteData {
const TopShellRoute();
static final GlobalKey<NavigatorState> $navigatorKey = shellNavigatorKey;
Widget builder(
BuildContext context,
GoRouterState state,
Widget navigator,
) {
return TopPage(child: navigator);
}
}
<HomeRoute>(
path: HomeRoute.path,
)
class HomeRoute extends GoRouteData {
const HomeRoute();
static const path = '/home';
Widget build(BuildContext context, GoRouterState state) => const HomePage();
}
<SettingsRoute>(
path: SettingsRoute.path,
)
class SettingsRoute extends GoRouteData {
const SettingsRoute();
static const path = '/settings';
Widget build(BuildContext context, GoRouterState state) =>
const SettingsPage();
}
lib/home_page.dart
を内部でカウンターのstateを持つように変更
2-4. -
hoge_page.dart
lib/hoge_page.dartimport 'package:flutter/material.dart'; class HomePage extends StatefulWidget { const HomePage({super.key}); State<HomePage> createState() => _HomePageState(); } class _HomePageState extends State<HomePage> { int _counter = 0; void _incrementCounter() { setState(() { _counter++; }); } Widget build(BuildContext context) { return Scaffold( body: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[ const Text( 'You have pushed the button this many times:', ), Text( '$_counter', style: Theme.of(context).textTheme.headlineMedium, ), ], ), ), floatingActionButton: FloatingActionButton( onPressed: _incrementCounter, tooltip: 'Increment', child: const Icon(Icons.add), ), ); } }
ここまできたら fvm flutter pub run build_runner build --delete-conflicting-outputs
を実施し、 router.g.dart
を生成しときます。
実行して以下の様に単純に Home Page
と Settings Page
が切り替わる様になっとけばOKです。
ただ先に述べた通り、タブを切り替えると操作した内容(state)がクリアされるのでカウンターが0に戻っていると思います。
3. BottomNavigationBarと連携 (状態が残るパターン)
StatefulShellBranch を作成
3-1. 各Sub Route (HomeRoute, SettingsRoute) 用のrouter.dart
に以下を追加します。
// ↓NavigatorStateを新規追加
final GlobalKey<NavigatorState> homeNavigatorKey = GlobalKey<NavigatorState>();
final GlobalKey<NavigatorState> settingsNavigatorKey =
GlobalKey<NavigatorState>();
// ...
class HomeShellBranchData extends StatefulShellBranchData {
const HomeShellBranchData();
static final GlobalKey<NavigatorState> $navigatorKey = homeNavigatorKey;
}
class SettingsShellBranchData extends StatefulShellBranchData {
const SettingsShellBranchData();
static final GlobalKey<NavigatorState> $navigatorKey = settingsNavigatorKey;
}
3-2. @TypedShellRoute を @TypedStatefulShellRoute に変更
<TopShellRoute>(
branches: <TypedStatefulShellBranch<StatefulShellBranchData>>[
TypedStatefulShellBranch<HomeShellBranchData>(
routes: <TypedRoute<RouteData>>[
TypedGoRoute<HomeRoute>(path: HomeRoute.path),
],
),
TypedStatefulShellBranch<SettingsShellBranchData>(
routes: <TypedRoute<RouteData>>[
TypedGoRoute<SettingsRoute>(path: SettingsRoute.path),
],
),
],
)
この時点では TopShellRoute
がエラーになっているかと思います。
StatefulShellRouteData を継承する様に修正
3-3. TopShellRoute クラスがclass TopShellRoute extends StatefulShellRouteData {
const TopShellRoute();
static final GlobalKey<NavigatorState> $navigatorKey = shellNavigatorKey;
Widget builder(
BuildContext context,
GoRouterState state,
StatefulNavigationShell navigator,
) {
return TopPage(child: navigator);
}
}
StatefulNavigationShell を受け取れる様に修正
3-4. TopPage がimport 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
enum PageIndex { home, settings }
class TopPage extends StatefulWidget {
const TopPage({required this.navigationShell, super.key});
final StatefulNavigationShell navigationShell;
State<TopPage> createState() => _TopPageState();
}
class _TopPageState extends State<TopPage> {
Widget build(BuildContext context) {
return Scaffold(
body: widget.navigationShell,
bottomNavigationBar: _bottomNavigationBar(context),
);
}
BottomNavigationBar _bottomNavigationBar(BuildContext context) {
return BottomNavigationBar(
currentIndex: widget.navigationShell.currentIndex,
items: const <BottomNavigationBarItem>[
BottomNavigationBarItem(
icon: Icon(Icons.home),
label: "Home",
),
BottomNavigationBarItem(
icon: Icon(Icons.settings),
label: "Settings",
),
],
onTap: (int index) {
widget.navigationShell.goBranch(
index,
initialLocation: index == widget.navigationShell.currentIndex,
);
},
);
}
}
先ほどまでの BottomNavigationBar
の currentIndex
を制御する目的の _selectedIndex
が StatefulNavigationShell
の currentIndex
に置き換わっています。
またタップ時の遷移では StatefulNavigationShell#goBranch を使用して遷移する様に変更してます。
ここまでできたら fvm flutter pub run build_runner build --delete-conflicting-outputs
を実施し router.g.dart
を生成しときます。
この時点の `router.dart`
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:go_router_example/hoge_page.dart';
import 'package:go_router_example/settings_page.dart';
import 'package:go_router_example/top_page.dart';
part 'router.g.dart';
final GlobalKey<NavigatorState> rootNavigatorKey = GlobalKey<NavigatorState>();
final GlobalKey<NavigatorState> shellNavigatorKey = GlobalKey<NavigatorState>();
final GlobalKey<NavigatorState> homeNavigatorKey = GlobalKey<NavigatorState>();
final GlobalKey<NavigatorState> settingsNavigatorKey =
GlobalKey<NavigatorState>();
final router = GoRouter(
routes: $appRoutes,
initialLocation: HomeRoute.path,
navigatorKey: rootNavigatorKey,
);
<TopShellRoute>(
branches: <TypedStatefulShellBranch<StatefulShellBranchData>>[
TypedStatefulShellBranch<HomeShellBranchData>(
routes: <TypedRoute<RouteData>>[
TypedGoRoute<HomeRoute>(path: HomeRoute.path),
],
),
TypedStatefulShellBranch<SettingsShellBranchData>(
routes: <TypedRoute<RouteData>>[
TypedGoRoute<SettingsRoute>(path: SettingsRoute.path),
],
),
],
)
class TopShellRoute extends StatefulShellRouteData {
const TopShellRoute();
static final GlobalKey<NavigatorState> $navigatorKey = shellNavigatorKey;
Widget builder(
BuildContext context,
GoRouterState state,
StatefulNavigationShell navigator,
) {
return TopPage(navigationShell: navigator);
}
}
class HomeShellBranchData extends StatefulShellBranchData {
const HomeShellBranchData();
static final GlobalKey<NavigatorState> $navigatorKey = homeNavigatorKey;
}
class SettingsShellBranchData extends StatefulShellBranchData {
const SettingsShellBranchData();
static final GlobalKey<NavigatorState> $navigatorKey = settingsNavigatorKey;
}
<HomeRoute>(
path: HomeRoute.path,
)
class HomeRoute extends GoRouteData {
const HomeRoute();
static const path = '/home';
Widget build(BuildContext context, GoRouterState state) => const HomePage();
}
<SettingsRoute>(
path: SettingsRoute.path,
)
class SettingsRoute extends GoRouteData {
const SettingsRoute();
static const path = '/settings';
Widget build(BuildContext context, GoRouterState state) =>
const SettingsPage();
}
実行してみると、今回はタブを切り替えても操作した内容(state)が残っており、カウンターの値がそのままになっているのが分かると思います。
4. fullscreenのダイアログを表示 (おまけ)
BottomNavigationBar内のページ上でボタンを押すとfullscreenのダイアログが表示されるパターンを実装して見たいと思います。設定画面(SettingsPage)上でカラーピッカーダイアログを表示する様なケースを想定して進めていきます。
4-1. ダイアログ用の画面を追加
lib/color_picker_dialog.dart
を以下内容で作成します。
import 'package:flutter/material.dart';
class ColorPickerDialog extends StatelessWidget {
const ColorPickerDialog({super.key});
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('ColorPickerDialog')),
body: Container(
color: Colors.deepPurple.shade300,
child: const Center(
child: Text('ColorPickerDialog'),
),
),
);
}
}
router.dart
修正
4-2. 以下を追加します。
<ColorPickerDialogRoute>(
path: ColorPickerDialogRoute.path,
)
class ColorPickerDialogRoute extends GoRouteData {
const ColorPickerDialogRoute();
static const path = '/color_picker_dialog';
Page<void> buildPage(BuildContext context, GoRouterState state) =>
MaterialPage<Object>(
fullscreenDialog: true,
key: state.pageKey,
child: const ColorPickerDialog(),
);
}
settings_page.dart
修正
4-3. import 'package:flutter/material.dart';
import 'package:go_router_example/router/router.dart';
class SettingsPage extends StatelessWidget {
const SettingsPage({super.key});
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: ElevatedButton(
onPressed: () => const ColorPickerDialogRoute().push(context),
child: const Text('Show Color Picker Dialog')),
),
);
}
}
ここまでできたら fvm flutter pub run build_runner build --delete-conflicting-outputs
を実施し router.g.dart
を生成し実行してみます。
↓ダイアログが表示されていればOKです。
リポジトリ
https://github.com/Slowhand0309/go_router_example
今回試した分は↑こちらに公開してます。
この記事は以下の情報を参考にして執筆しました
- 【Flutter】go_router をタイプセーフに使う方法【go_router_builder】
- Integrating Bottom Navigation with Go Router in Flutter | by onat çipli | Flutter Community | Sep, 2023 | Medium
- packages/packages/go_router_builder/example/lib/shell_route_example.dart at main · flutter/packages
- 【Flutter】 go_routerでTabBarView(+α in BottomNavigationBar)の画面遷移の方法
- [続] go_routerでBottomNavigationBarの永続化に挑戦する(StatefulShellRoute)
Discussion