はじめに
go_router_builderを使ってみようということで、使用する利点としては主に以下が挙げられると思います。
-
型安全性の向上
ルーティングの設定が型安全になり、画面間の移動で必要な情報が正しく渡されているかを、アプリを実行する前にチェックできます。 -
コードの自動生成
ルーティングに必要なコードが自動で生成され、開発プロセスの効率化が図れます。
この記事では、これらの利点に加え、Riverpodを用いたアプリ全体の状態管理を組み合わせて、ボトムナビゲーションバーの実装をします。
環境
- Flutter 3.16.9
- パッケージは以下を参照(初期から追加したもののみ記載しています)
dependencies:
flutter_riverpod: ^2.4.9
go_router: ^12.1.1
dev_dependencies:
build_runner: ^2.4.7
go_router_builder: ^2.3.4
プロジェクト構成
- main.dart: アプリケーションのエントリーポイント。ルートウィジェットを定義
-
router: go_router_builderを使用して定義されたアプリケーションのルートを管理するディレクトリ
-
branch: ナビゲーションの分岐を管理するディレクトリ
- home_branch.dart: ホーム画面へのナビゲーションフローを定義
- setting_branch.dart: 設定画面へのナビゲーションフローを定義
-
routes.dart: アプリケーションの全ルートを集約するファイル
※後に説明しますが、go_router_builderを使用してルートを定義し、build_runnerによってroutes.g.dartにコードが生成されます。
-
branch: ナビゲーションの分岐を管理するディレクトリ
- pages: アプリケーションの各画面を定義するファイルが格納
実装
1. ボトムナビゲーションの設定とルーティングの構築
ボトムナビゲーションとそれに伴うルーティングの設定は、routes.dart
に記述していきます。このファイルでは、アプリケーションの主要なナビゲーションパスと、それぞれのパスに対応する画面を定義します。
(1) まずはroutes.dartの全コード
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
part 'routes.g.dart';
part 'branch/home_branch.dart';
part 'branch/setting_branch.dart';
final _rootNavigatorKey = GlobalKey<NavigatorState>();
final routerProvider = Provider<GoRouter>((ref) {
return GoRouter(
initialLocation: '/home', // 初期ルート
navigatorKey: _rootNavigatorKey, // ルートナビゲーターキー
debugLogDiagnostics: kDebugMode, // デバッグモードでのみログを出力
routes: $appRoutes, // 生成されたルート
);
});
// メインシェルルート定義
@TypedStatefulShellRoute<MainShellRouteData>(
branches: <TypedStatefulShellBranch<StatefulShellBranchData>>[
homeStatefulShellBranch,
settingStatefulShellBranch,
],
)
// メインシェルルートの状態
class MainShellRouteData extends StatefulShellRouteData {
const MainShellRouteData();
@override
Widget builder(
BuildContext context,
GoRouterState state,
StatefulNavigationShell navigationShell,
) {
return AppNavigationBar(navigationShell: navigationShell);
}
}
// ナビゲーションバー
class AppNavigationBar extends StatelessWidget {
const AppNavigationBar({
super.key,
required this.navigationShell,
});
final StatefulNavigationShell navigationShell;
@override
Widget build(BuildContext context) {
return Scaffold(
body: navigationShell,
bottomNavigationBar: NavigationBar(
selectedIndex: navigationShell.currentIndex,
destinations: const [
NavigationDestination(
icon: Icon(Icons.home),
label: 'home',
),
NavigationDestination(
icon: Icon(Icons.settings),
label: 'settings',
),
],
onDestinationSelected: _goBranch,
),
);
}
// タブ選択時の処理
void _goBranch(int index) {
navigationShell.goBranch(
index,
initialLocation: index == navigationShell.currentIndex,
);
}
}
(2) 解説
① GoRouterの設定
final _rootNavigatorKey = GlobalKey<NavigatorState>();
final routerProvider = Provider<GoRouter>((ref) {
return GoRouter(
initialLocation: '/home',
navigatorKey: _rootNavigatorKey,
debugLogDiagnostics: kDebugMode,
routes: $appRoutes,
);
});
まず、アプリケーションのナビゲーションを管理するためにGoRouterインスタンスを作成します。このインスタンスは、アプリケーションのルートとなるナビゲーションロジックを定義するために使用されます。
② StatefulShellRouteの追加
// メインシェルルート定義
@TypedStatefulShellRoute<MainShellRouteData>(
branches: <TypedStatefulShellBranch<StatefulShellBranchData>>[
homeStatefulShellBranch,
settingStatefulShellBranch,
],
)
class MainShellRouteData extends StatefulShellRouteData {
// 省略...
}
@TypedStatefulShellRoute
アノテーションを使用して、アプリケーションのメインナビゲーションシェルを定義しています。
@TypedStatefulShellRoute
アノテーションは、アプリケーションにおけるナビゲーションの構造を定義するために使用されます。
このアノテーションを使って指定された部分は、アプリのメインナビゲーション(画面間の移動を管理する中心的な部分)として機能します。
@TypedStatefulShellRoute
アノテーションは、必ずStatefulShellRouteDataを拡張するクラスの直前に記述してください。go_router_builderパッケージは、このアノテーションが適用された直下のクラスを基にコードを自動生成します。アノテーションの位置を間違えると、正しくコードが生成されないため注意が必要です。
branches
プロパティには、ボトムナビゲーションバーの各タブに対応するナビゲーションの分岐を指定します。
ここでのhomeStatefulShellBranch
とsettingStatefulShellBranch
は、それぞれホーム画面と設定画面へのナビゲーションフローを管理する分岐を表しています。
これらの分岐は、home_branch.dartとsetting_branch.dartといった別のファイルでモジュール化しています。後ほど、これらの分岐の設定について詳しく説明します。
<TypedStatefulShellBranch<StatefulShellBranchData>>
は、アプリケーションのナビゲーションフローの中で、特定のナビゲーションパス(例えばホーム画面や設定画面への遷移)を管理するための「分岐」を定義するために使用されます。
TypedStatefulShellBranchは、ナビゲーションの分岐を表しており、
StatefulShellBranchDataは、それぞれの「分岐」が持つべき情報や状態を定義します。
例えば、ホーム画面への分岐では、ホーム画面特有の情報を、設定画面への分岐では、設定画面特有の情報を持たせることができます。
③ ボトムナビゲーションバーの実装
StatefulShellRoute内で、NavigationBarを実装したウィジェットをルート画面として配置します。
class MainShellRouteData extends StatefulShellRouteData {
const MainShellRouteData();
@override
Widget builder(
BuildContext context,
GoRouterState state,
StatefulNavigationShell navigationShell,
) {
return AppNavigationBar(navigationShell: navigationShell);
}
}
// ナビゲーションバー
class AppNavigationBar extends StatelessWidget {
const AppNavigationBar({
super.key,
required this.navigationShell,
});
final StatefulNavigationShell navigationShell;
@override
Widget build(BuildContext context) {
return Scaffold(
body: navigationShell,
bottomNavigationBar: NavigationBar(
selectedIndex: navigationShell.currentIndex,
destinations: const [
NavigationDestination(
icon: Icon(Icons.home),
label: 'home',
),
NavigationDestination(
icon: Icon(Icons.settings),
label: 'settings',
),
],
onDestinationSelected: _goBranch,
),
);
}
// タブ選択時の処理
void _goBranch(int index) {
navigationShell.goBranch(
index,
initialLocation: index == navigationShell.currentIndex,
);
}
}
④ partの設定
partキーワードを使用することで、Dartでは複数のファイルを1つのライブラリとしてグループ化することができます。
part 'routes.g.dart';
part 'branch/home_branch.dart';
part 'branch/setting_branch.dart';
-
part 'routes.g.dart';
routes.dartファイルにroutes.g.dartファイルを含めることを示しています。(build_runnerコマンドを実行することで自動的に生成されるファイル) -
part 'branch/home_branch.dart';
ホーム画面に関連するナビゲーションの分岐(branch)を定義するファイルです。 -
part 'branch/setting_branch.dart';
設定画面に関連するナビゲーションの分岐を定義するファイルです。
2. ホームタブのナビゲーション設定
home_branch.dart
はアプリケーションのホームタブに関連するナビゲーションロジックを定義する場所です。
このファイルでは、ホーム画面へのルートと、ホーム画面から派生するサブページへのルートが設定されています。
(1) home_branch.dartの全コード
part of '../routes.dart';
// ホームタブの定義
class HomeShellBranch extends StatefulShellBranchData {
const HomeShellBranch();
}
// ホームタプの状態
const homeStatefulShellBranch = TypedStatefulShellBranch<HomeShellBranch>(
routes: <TypedRoute<RouteData>>[
TypedGoRoute<HomePageRoute>(
path: '/home',
routes: [
TypedGoRoute<Sample1PageRoute>(
path: 'sample1',
),
TypedGoRoute<Sample2PageRoute>(
path: 'sample2',
),
],
),
],
);
class HomePageRoute extends GoRouteData {
const HomePageRoute();
@override
Widget build(BuildContext context, GoRouterState state) {
return const HomePage();
}
}
class Sample1PageRoute extends GoRouteData {
const Sample1PageRoute();
@override
Widget build(BuildContext context, GoRouterState state) {
return const Sample1Page();
}
}
class Sample2PageRoute extends GoRouteData {
const Sample2PageRoute();
static final GlobalKey<NavigatorState> $parentNavigatorKey =
_rootNavigatorKey;
@override
Widget build(BuildContext context, GoRouterState state) {
return const Sample2Page();
}
}
(2) 解説
① HomeShellBranchの定義
// ホームタブの定義
class HomeShellBranch extends StatefulShellBranchData {
const HomeShellBranch();
}
HomeShellBranchクラスは、ホームタブのナビゲーション分岐を表します。
このクラスはStatefulShellBranchData
を継承しており、ホームタブに関連するナビゲーションの状態を管理します。
② 各ページのルート定義
class HomePageRoute extends GoRouteData {
const HomePageRoute();
@override
Widget build(BuildContext context, GoRouterState state) {
return const HomePage();
}
}
class Sample1PageRoute extends GoRouteData {
const Sample1PageRoute();
@override
Widget build(BuildContext context, GoRouterState state) {
return const Sample1Page();
}
}
class Sample2PageRoute extends GoRouteData {
const Sample2PageRoute();
@override
Widget build(BuildContext context, GoRouterState state) {
return const Sample2Page();
}
}
HomePageRoute、Sample1PageRoute、Sample2PageRouteクラスは、それぞれホーム画面、サンプルページ1、サンプルページ2へのルートを定義しています。
これらのクラスはGoRouteData
を継承しており、buildメソッド内で対応するウィジェット(ページ)を返します。これにより、指定されたパスにナビゲートされたときに、適切な画面が表示されるようになります。
③ ホームタブのルーティング設定
// ホームタプの状態
const homeStatefulShellBranch = TypedStatefulShellBranch<HomeShellBranch>(
routes: <TypedRoute<RouteData>>[
TypedGoRoute<HomePageRoute>(
path: '/home',
routes: [
TypedGoRoute<Sample1PageRoute>(
path: 'sample1',
),
TypedGoRoute<Sample2PageRoute>(
path: 'sample2',
),
],
),
],
);
homeStatefulShellBranchは、ホームタブのルーティングを定義する定数です。
TypedStatefulShellBranch
を使用して、ホーム画面へのルート(/home)と、ホーム画面から派生するサブページ(sample1、sample2)へのルートを設定しています。
3. 設定タブのナビゲーション設定
こちらについてはほとんどホームタブと同じなので、説明は割愛します。
part of '../routes.dart';
// ホームタブの定義
class SettingShellBranch extends StatefulShellBranchData {
const SettingShellBranch();
}
// ホームタプの状態
const settingStatefulShellBranch = TypedStatefulShellBranch<SettingShellBranch>(
routes: <TypedRoute<RouteData>>[
TypedGoRoute<SettingPageRoute>(
path: '/setting',
),
],
);
class SettingPageRoute extends GoRouteData {
const SettingPageRoute();
@override
Widget build(BuildContext context, GoRouterState state) {
return const SettingPage();
}
}
4. コードの自動生成
以下でコードを自動生成します。
これにより、routes.g.dartにコードが生成されます。
flutter pub run build_runner build --delete-conflicting-outputs
5. ルートウィジェットを定義
routerConfigプロパティにGoRouterインスタンスを渡すことで、アプリケーションのルーティング設定が適用されます。
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router_sample/router/routes.dart';
void main() {
runApp(
const ProviderScope(
child: MyApp(),
),
);
}
class MyApp extends ConsumerWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final router = ref.watch(routerProvider);
return MaterialApp.router(
routerConfig: router,
);
}
}
6. ページ遷移
go_router_builderを使用したことにより、Sample1PageRoute().push<void>(context);
のようなシンプルなコードでページ遷移を行うことができるようになりました。
import 'package:flutter/material.dart';
import 'package:go_router_sample/router/routes.dart';
class HomePage extends StatelessWidget {
const HomePage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
ElevatedButton(
onPressed: () {
const Sample1PageRoute().push<void>(context);
},
child: const Text('Sample Page 1'),
),
const SizedBox(height: 20),
ElevatedButton(
onPressed: () {
const Sample2PageRoute().push<void>(context);
},
child: const Text('Sample Page 2'),
),
],
),
),
);
}
}
ボトムナビゲーションバーの表示制御
アプリケーション内でボトムナビゲーションバーを隠す画面と隠さない画面を出し分けることも可能です。GlobalKeyを活用することで、このような挙動の制御が可能になります。
実装
例えばhome_branch.dartのSample2PageRouteに以下の行を追加します。
class Sample1PageRoute extends GoRouteData {
const Sample1PageRoute();
@override
Widget build(BuildContext context, GoRouterState state) {
return const Sample1Page();
}
}
class Sample2PageRoute extends GoRouteData {
const Sample2PageRoute();
+ static final GlobalKey<NavigatorState> $parentNavigatorKey = _rootNavigatorKey;
@override
Widget build(BuildContext context, GoRouterState state) {
return const Sample2Page();
}
}
この$parentNavigatorKey
は、Sample2Pageが表示される際にメインのナビゲーションスタック(_rootNavigatorKeyに紐づいているもの)を参照することを示しています。
これにより、Sample2Pageが表示される際には、メインのナビゲーションスタックが使用され、その結果としてボトムナビゲーションバーが隠される挙動を実現できます。
コードの自動生成を忘れずに。
flutter pub run build_runner build --delete-conflicting-outputs
挙動
通常 | ボトムナビゲーションバーを隠す |
---|---|
参考