Flutter初心者のためのGoRouterパッケージ
Flutterのナビゲーションにはたくさんの選択肢がありますが、今回はGoRouterパッケージについて解説します。GoRouterはFlutterの公式が推奨するナビゲーション管理パッケージで、柔軟性に富んだルーティングを実現できます。
GoRouterの基本的な実装流れ
- パッケージ追加
pubspec.yamlファイルにgo_router
の依存関係を追加します。
dependencies:
go_router: ^6.0.1
- ルート情報を定義
routes
リストにページごとのルート情報を定義します。path
でURLパスを指定し、builder
で対応する画面ウィジェットを返します。
// router.dart
final GoRouter _router = GoRouter(
routes: <GoRoute>[
GoRoute(
path: '/',
builder: (BuildContext context, GoRouterState state) => HomeScreen(),
),
GoRoute(
path: '/details',
builder: (BuildContext context, GoRouterState state) => DetailsScreen(),
),
],
);
- アプリケーションのメインルートでGoRouterを設定
MaterialApp.router
コンストラクタを使って、GoRouterの設定を行います。
class Main extends StatelessWidget {
const Main({super.key});
Widget build(BuildContext context) {
return MaterialApp.router( // routerを設定
routerConfig: router,
theme: ThemeData(
appBarTheme: AppBarTheme(
color: Colors.amber[400],
),
),
);
}
}
- onPressedでcontext.go('/path')を呼び出して画面遷移
ボタンのonPressedなどで、context.go('/path')
を呼び出すことで画面遷移ができます。
ElevatedButton(
onPressed: () => context.go('/details'),
child: Text('Go to Details'),
),
GoRouterの高度な機能
GoRouterには基本的な画面遷移だけでなく、高度なナビゲーション機能がたくさんあります。公式ドキュメントのNavigationを参照すると、さまざまな種類の遷移について学ぶことができます。
-
context.go
:単純な画面遷移 -
context.push
:スタックに積む遷移 -
context.pushNamed
:名前付きルートへの遷移 -
ShellRoute
:アプリの骨格を構成するルート
特にShellRoute
は便利で、アプリ全体で共通するUI(AppBarなど)を一か所で管理できるようになります。
GoRouterの設定方法
GoRouterの設定には主に2つの方法があります。
-
routerDelegate
とrouterInformationParser
を渡す方法
MaterialApp.router(
routerDelegate: router.routerDelegate,
routerInformationParser: router.routerInformationParser,
)
-
routerConfig
にGoRouterインスタンスを渡す方法(推奨)
MaterialApp.router(
routerConfig: router,
)
StatefulShellRouteを使ったBottomNavigationBarの永続表示
BottomNavigaitonBarは、StatefulShellRoute.indexStackを使用することで、比較的簡単に実装ができます。
実装する上で、理解しておく必要があるものを書いておくと、以下5つになります。
- final _rootNavigatorKey = GlobalKey<NavigatorState>();
- StatefulShellRoute.indexedStack
- StatefulShellRoute.indexedStackのbranches
- StatefulShellBranch
- StatefulShellBranchのGoRoute
字面だけなのでイメージしづらかったのですが、この方が作成しているイメージ図が分かりやすかったので、載せておきます。
僕が持っているイメージとしては、以下です。
- StatefulShellRoute.indexedStackで、各タブを管理する箱を作る
- indexedStackのbranchesで、各タブ内で独立した箱を作る
- StatefulShellBranchのGoRouteで、独立した箱の中での画面遷移を実現する
このイメージを持ちつつ実装していきます。
1. ルートナビゲーターキーの定義
最初に、アプリケーションのナビゲーション状態を管理するためのルートナビゲーターキーを作成します。
final _rootNavigatorKey = GlobalKey<NavigatorState>();
- このキーは、アプリのナビゲーションスタック全体を管理するために使われます。
- アプリがこの「どの画面に行ったかの履歴」をきちんと管理し、ユーザーが簡単に前に戻ったり、新しい画面に進んだりできるようにすることを意味してます。
- このルートナビゲーターキーを使うことで、アプリ全体のナビゲーション状態を一元的に管理できるようになります。
- 逆にルートナビゲーターキーを定義しないと、ユーザーが前の画面に戻ろうとしたときに、適切に遷移できなくなる可能性が出てきます。
2. StatefulShellRoute.indexedStack
次に、StatefulShellRoute.indexedStack
を使用して、複数のナビゲーションブランチを持つ箱を作成します。
StatefulShellRoute.indexedStack(
builder: (context, state, navigationShell) {
return TodoBottomNavigationBar(navigationShell: navigationShell);
},
branches: [ ... ],
)
-
TodoBottomNavigationBar
はアプリの下部にあるナビゲーションバーのことです。navigationShell
を通じてナビゲーションを管理します。コードを載せておくと、以下です。 - ボトムナビゲーションバー専用のクラスを作成して、それをアプリ全体で表示させます。
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
class TodoBottomNavigationBar extends StatelessWidget {
const TodoBottomNavigationBar({
super.key,
required this.navigationShell,
});
final StatefulNavigationShell navigationShell;
Widget build(BuildContext context) {
return Scaffold(
body: navigationShell,
bottomNavigationBar: BottomNavigationBar(
currentIndex: navigationShell.currentIndex,
onTap: (selectedIndex) {
// ここでcurrentIndexを更新し、適切な画面に遷移する
navigationShell.goBranch(
selectedIndex,
initialLocation: selectedIndex == navigationShell.currentIndex,
);
},
items: const [
BottomNavigationBarItem(icon: Icon(Icons.home), label: 'home'),
BottomNavigationBarItem(icon: Icon(Icons.add), label: 'add'),
],
),
);
}
}
3. StatefulShellRoute.indexedStackのbranches
indexedStack
内で、異なるナビゲーションセクション(branches)、いわゆる「各タブ内で独立した箱」を定義します。
branches: [
StatefulShellBranch(...),
StatefulShellBranch(...),
]
- 各
StatefulShellBranch
はアプリの一部分を表し、それぞれ独立したナビゲーションスタックを持ちます。 - 各ブランチは独立したナビゲーションスタックを持っているため、ブランチ間で直接データを受け渡すことはできません。
- 必要に応じて、ブランチ間でデータを共有するための状態管理の仕組み(例えば、RiverpodなどのStateProvider)を導入する必要が出てきます。
- ブランチ間の遷移は、
context.pushNamed()
やcontext.goNamed()
などのメソッドを使います。
4. StatefulShellBranch
StatefulShellBranch
を使用して、ナビゲーションブランチを具体的に定義します。
StatefulShellBranch(
navigatorKey: _shellNavigatorHomeKey,
routes: [ ... ],
)
- ここで
navigatorKey
は、そのブランチのナビゲーションを一意に識別するために使用されます。
5. StatefulShellBranchのGoRoute
最後に、各StatefulShellBranch
内でGoRoute
を定義して、具体的なナビゲーションパスとそれに対応する画面を設定します。
GoRoute(
path: '/',
builder: (BuildContext context, GoRouterState state) => const TodoListPage(),
)
- ここで、
path
はナビゲーションのパスを指し、builder
はそのパスにアクセスしたときに表示されるウィジェットを生成します。ここではTodoListPageを返しています。
全体コード
これでアプリ全体にBottomNavigationBarを表示させることができたはずです。
各タブが独立しているので、別タブをタップして前のタブに再び戻ったときも、前表示されていた状態が維持されていると思います。
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:todo_riverpod/entities/todo_item.dart';
import 'package:todo_riverpod/todo_bottom_navigation_bar.dart';
import 'package:todo_riverpod/ui/add_todo_page.dart';
import 'package:todo_riverpod/ui/todo_list_page.dart';
import 'package:todo_riverpod/ui/update_todo_page.dart';
final _rootNavigatorKey = GlobalKey<NavigatorState>();
final _shellNavigatorHomeKey = GlobalKey<NavigatorState>(debugLabel: 'home');
final _shellNavigatorAddKey = GlobalKey<NavigatorState>(debugLabel: 'add');
final router = GoRouter(
navigatorKey: _rootNavigatorKey,
initialLocation: '/',
routes: <RouteBase>[
StatefulShellRoute.indexedStack(
builder: (context, state, navigationShell) {
return TodoBottomNavigationBar(navigationShell: navigationShell);
},
branches: [
// homeブランチ
StatefulShellBranch(
navigatorKey: _shellNavigatorHomeKey,
routes: <RouteBase>[
GoRoute(
path: '/',
builder: (BuildContext context, GoRouterState state) =>
const TodoListPage(),
),
GoRoute(
path: '/updateTodo',
builder: (context, state) {
final todoItem =
state.extra as TodoItem? ?? TodoItem(todoId: -1);
return UpdateTodoPage(todoItemProps: todoItem);
},
),
],
),
// addブランチ
StatefulShellBranch(
navigatorKey: _shellNavigatorAddKey,
routes: <RouteBase>[
GoRoute(
path: '/addTodo',
builder: (BuildContext context, GoRouterState state) =>
const AddTodoPage(),
),
],
),
],
),
],
);
上記の手順を踏むことで、BottomNavigationBarを使ったナビゲーションをFlutterアプリに実装できます。各ブランチは独立しているため、ユーザーがアプリ内で異なるセクションに移動しても、それぞれのナビゲーション状態は保持されるので、そこが嬉しいですね。
Discussion