【Flutter】go_routerでBottomNavigationBarの永続化に挑戦する
背景
界隈で話題のNavigation2.0を簡単に実装出来るgo_router
パッケージ
コード量も短く、比較的簡単に実装出来るのでとても良いと感じているのですが、使ってみた方に感想を聞いてみるとBottomNavigationBar
やTabBar
などの永続化が出来ず、そこが難点という話を複数名の方から伺いました
ネットを探っても同様のUIを実現したいという投稿は沢山ありますが、明確な答えはほとんど発見できませんでした
まだ使い始めだったのでgo_router
の勉強も兼ねて、この問題に挑戦してみた結果の共有をしてみたいと思います
あくまでも自分なりのアプローチであり、問題点なども洗い出せているわけではないので、自由研究の発表くらいだと思って愛のあるご指摘頂ければと思います
環境
SDK・パッケージ | バージョン |
---|---|
Flutter | 3.0.4 • channel stable |
Dart | 2.17.5 (stable) |
flutter_riverpod | ^1.0.4 |
go_router | ^4.1.0 |
コード全文
アプローチ
※ こちらはNavigation1.0とNavigation2.0を併用させたアプローチになります
-
GoRouter
クラスのnavigationBuilder
を活用 -
Navigator
クラスでラップしたBottomNavigationBar
をメインのページスタックの上に表示 - タブでの画面遷移に見える様、ページ遷移時のアニメーションを調整
navigationBuilder
パラメータではメインのページスタックの上にWidgetを重ねる事が出来ます。
こちらを活用し、BottomNavigationBar
を全てのページスタックの上に重ねて表示しています。
BottomNavigationBar
はMaterialPage
配下に配置されている必要がある為、Navigator
クラスでラップする必要があります。
肝となるのはGoRouterの以下部分です。
GoRouter(
...,
navigatorBuilder: (context, state, child) {
return Navigator(
onPopPage: (route, dynamic __) => false,
pages: [
MaterialPage<Widget>(
child: BottomNav(
child: child,
),
),
],
);
},
)
またタブでの画面遷移に見える様、トップレベルのページのみ遷移時のアニメーションを調整しています。
GoRoute(
name: 'simple',
path: '/simple',
pageBuilder: (context, state) => CustomTransitionPage(
key: state.pageKey,
child: const SimpleNavigationScreen(),
transitionDuration: Duration.zero, // <= Duration.zeroで遷移する様、調整
transitionsBuilder:
(context, animation, secondaryAnimation, child) => child),
ページ別動作のサンプル
BottomNavigationBarの各タブではそれぞれ異なる画面遷移のサンプルを用意しました。
Simple Navigation Screen
サブルートへの画面遷移
GoRoute(
name: 'simple',
...,
routes: [ // routes内に定義する事でsub-routeを繋げる
GoRoute(
name: 'login',
path: 'login',
pageBuilder: (context, state) => MaterialPage(
key: state.pageKey,
child: const LoginScreen(),
),
),
...,
],
),
引数を渡した画面遷移
GoRoute(
name: 'simple',
...,
routes: [
...,
GoRoute(
name: 'number',
path: 'number/:id', // 引数を含むパスを定義
builder: (context, state) {
final id = state.params['id']!; // stateから引数を取り出す
return NumberScreen(number: id);
},
),
],
),
引数を含むパスで遷移
onTap: () => context.go('/simple/number/$id');
Overlay Navigation Screen
Dialog
やModalBottomSheet
の挙動のサンプルです。
Dialog
やModalBottomSheet
はGoRouter
で定義されたページスタックではなく、navigatorBuilder
内に定義されたNavigator
クラス上に被さります
その為、これらをクローズする場合はGoRouter.of(context).pop()
ではなく、Navigator.of(context).pop()
を使います
await showDialog<void>(
context: context,
builder: (context) {
return AlertDialog(
title: const Text('This is the Dialog'),
content: const Text('This is the content'),
actions: [
TextButton(
child: const Text('Cancel'),
onPressed: () => Navigator.of(context).pop(), // Navigatorのpopを使用
),
TextButton(
child: const Text('OK'),
onPressed: () => Navigator.of(context).pop(),
),
],
);
},
);
またshowModalBottomSheet
ではuseRootNavigator
パラメータを使う事で、Navigator
クラスの上に被せるか、GoRouter
クラスの上に被せるかを操作出来ます
await showModalBottomSheet<bool>(
context: context,
// trueでNavigatorの上、デフォルトのfalseでGoRouterの上に表示
useRootNavigator: true,
builder: (context) {
...
},
);
TabBar Navigation Screen
BottomNavigationBar
とTabBar
を併用した挙動のサンプルです。
TabBarView
内からネストした画面へも問題なく遷移出来ます
問題点
・ブラウザバックに非対応
本アプローチではBottomNavigationBar
の選択中インデックスをアイコンをタップする事で変更しています。
その為、Webのブラウザバックに対し、選択中インデックスの更新がされません。
Navigation2.0だけでの対応
本アプローチはNavigation1.0とNavigation2.0を併用したアプローチになります。
Navigationr1.0と2.0の併用については公式ドキュメントでも「併用可」とする記述がある為、問題はないと思いますが、可能であれば一本化したい所。
ですが現状、Nabigation2.0のみでTabBar
やBottomNavigationBar
の永続化は行えません。
ただ以下の通り、対応について度々議論されている為、将来的に対応される可能性は十分ありそうです。
Discussion