iOS版のTwitterなどで使われているお馴染みのUIです。ググれば様々な実装方法が出てきますが、たくさんあって混乱するので一度ここにまとめたいと思います。
![twitter](https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F3112065%2F44165578-3b9f-47e8-c0d6-22583f7f88ed.gif?ixlib=rb-4.0.0&auto=format&gif-q=60&q=75&s=934642737a76fd62d5fa20971720ece7)
1. CupertinoTab系のWidgetを使う
FlutterにはiOS風のUIを再現するためにCupertino*
と名のつくWidget群が用意されており、その中のCupertinoTabScaffold
やCupertinoTabBar
等を使うと固定タブバーが実現できます。この方法については以下の記事が詳しいです。
また公式ドキュメントにはブラウザで動かせるインタラクティブな実装例が掲載されているのでオススメです。
![cupertino](https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F3112065%2Fa09c975d-164d-8921-b6e7-f12fd83144b6.gif?ixlib=rb-4.0.0&auto=format&gif-q=60&q=75&s=ef2273648b50323f92740ce8bc4a53a5)
ただしこの方法だとCupertinoTabBar
以外のタブバーが使えません。BottomNavigationBarやanimated_bottom_navigation_bar、curved_navigation_barのようなサードパーティー製のタブバーなどを使用したい場合は他の手法を選択する必要があります。
2. persistent_bottom_nav_barを使う
読んで字の如しなパッケージがあります。タブバーの見た目にはいくつか選択肢があり、かなり柔軟に使用できそうです。また拡張することで自分好みのスタイルも作ることができるようです。
3. go_routerを使う
go_routerはNavigator2.0を簡単に利用できるルーティングライブラリです。go_routerではShellRoute
による階層的なナビゲーションもサポートされており、これにより固定タブバーの永続化が実現できます。日本語記事では
あたりが分かりやすいかと思います。公式にもサンプルが用意されているため、go_routerをすでに使用している場合はこれを利用するのが一番楽かもしれません。
ただし、現状ShellRoute
は各タブの状態を保持してくれません。例えばホームタブで作業したあと検索タブに遷移し、その後ホームタブに再び戻ると作業状態がリセットされてしまいます。
![shellroute](https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F3112065%2F03504e76-89bb-72ff-af09-9c7893df9460.gif?ixlib=rb-4.0.0&auto=format&gif-q=60&q=75&s=af7b95ec47fa6fd499e50df840c68461)
この問題についてはすでにGitHub上でissueが挙がっており、現在はプルリクエストのマージ待ちのようです。
追記(2023/5/27)
マージされたようですね。v7.1から使えるそうです。
4. navigator_scopeを使う
宣伝です。このために記事を書いたようなものです。前述のCupertinoTab*
で実装されている入れ子のナビゲーション機能を抜き出し、より一般化してパッケージにまとめました。
とにかくシンプルなことが売りです。Navigator2.0は複雑すぎ、Navigation.push
してNavigation.pop
するだけで十分、という場合におすすめです。新しいAPIを学ぶ必要もありません。またBottomNavigationBarだけでなく、NavigationBarやNavigationDrawer(横から飛び出すメニュー)、さらにはanimated_bottom_navigation_barやcurved_navigation_barなどの著名なサードパーティー製のタブバーも自由に利用できます。
![navigatorscope](https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F3112065%2F8d3031e4-7078-b0de-eac8-d0be5b9afb8a.gif?ixlib=rb-4.0.0&auto=format&gif-q=60&q=75&s=7726a80289bbf7d8c070a13e8576c17f)
使い方は簡単で、NavigatorScope
の中で各タブのNestedNavigatorを作るだけです。もちろん画面遷移の前後で各タブの状態は保持されます。
Scaffold(
bottomNavigationBar: NavigationBar( // お好きなタブバーをひとつ
selectedIndex: selectedTabIndex,
destinations: tabs,
onDestinationSelected: (index) {
setState(() => selectedTabIndex = index);
},
...
),
body: NavigatorScope( // ローカルなNavigatorのハブとなるWidget
currentDestination: selectedTabIndex, // 現在アクティブなタブのインデックス
destinationCount: tabs.length,
destinationBuilder: (context, index) {
return NestedNavigator( // ローカルなNavigator
// このNavigatorのデフォルトのページを作成する
builder: (context) => Container(),
);
},
),
);
タブを切り替えるときはcurrentDestination
の値を変えてNavigatorScope
をrebuildするだけです。タブ内で画面遷移する場合は通常通りNavigator.push
やNavigator.pop
を使用します。詳しくはREADMEをご覧ください。
5. 自前で実装する
最終手段です。Deep linkingやらNavigator2.0やらを考慮しだすと複雑になりますが、基本的な部分だけを抜き出してみると案外簡単に実装できます。
CupertinoTab*の実装を理解する
前述の通り、CupertinoTab*
系のWidget群はデフォルトで固定タブバーの機能を提供しています。特に重要なのはCupertinoTabView
とCupertinoTabScaffold
で、前者はタブのNavigator
を内部で保持し、後者は各タブの切り替えとそのNavigator
の状態保持を担当しています。Widgetツリーのイメージとしては下のような感じです。
CupertinoTabScaffold
└ _TabSwitchingView
├ CupertinoTabView
│ └ Navigator
├ CupertinoTabView
...
_TabSwitchingView
はCupertinoTabScaffold
内で使われているプライベートなクラスで、Navigator
の切り替えや状態保持を実質的に担当しているWidgetです。またこれらは全てStatefulWidgetとして定義されているので、以後「CupertinoTabView
のbuild
メソッド」のような表記がされていた場合は「対応するState
クラスのbuild
メソッド」と読み替えてください。
CupertinoTabView
細かい話を抜きにすれば、CupertinoTabView
がしていることはNavigator
を1つ保持する、ただそれだけです。実際build
メソッドは以下のようにとてもシンプルな実装です(ソースコード)。
// _CupertinoTabViewState
@override
Widget build(BuildContext context) {
return Navigator(
key: widget.navigatorKey,
onGenerateRoute: _onGenerateRoute,
onUnknownRoute: _onUnknownRoute,
observers: _navigatorObservers,
restorationScopeId: widget.restorationScopeId,
);
}
初期ページを作成したり新しいページがNavigator.pushNamed
されたりするとonGenerateRoute
が呼ばれます。ここで作成されたRoute
(デフォルトではCupertinoPageRoute
)がページスタックに積まれることになるのですが、今回は関係ないので割愛します。
CupertinoTabScafold
各タブのNavigator
(CupertinoTabView
)はCupertinoTabScaffold
内の_TabSwitchingView
が管理しています。CupertinoTabScaffold
のbuild
メソッドを見てみましょう(ソースコード)。重要な部分以外は省略しました。_controller.index
が現在アクティブなタブのインデックスを表しており、これが変わるとsetState
でrebuildが走るように実装されています。また各Nvigator
はwidget.tabBuilder
を通して_TabSwitchingView
に渡されます。
// _CupertinoTabScaffoldState
@override
Widget build(BuildContext context) {
...
Widget content = _TabSwitchingView(
currentTabIndex: _controller.index, // これが変わるとrebuild
tabCount: widget.tabBar.items.length,
tabBuilder: widget.tabBuilder, // 各タブのCupertinoTabViewを作成するbuilder
);
...
return DecoratedBox(
...
child: Stack(
children: <Widget>[
content,
...
],
),
);
}
最後に_TabSwitchingView
のbuild
メソッドも見てみましょう(ソースコード)。こちらも至ってシンプルです。ネストが少々深いですが、要点は「前述のtabBuilder
が作るCupertinoTabView
をOffstage
で囲み、Stack
で重ねる」ことです。Offstage
を使うのは非アクティブなタブを非表示にするためです。HeroMode
やTickerMode
にも囲まれていますが、これは非表示タブのアニメーションを無効化するためです。親のCupertinoTabScaffold
に巻き込まれてrebuildされると各タブのactive = index == widget.currentTabIndex;
の値が更新され、タブが切り替わります。
// _TabSwitchingViewState
@override
Widget build(BuildContext context) {
return Stack(
fit: StackFit.expand,
children: List<Widget>.generate(widget.tabCount, (int index) {
// 各タブがアクティブかどうか?
final bool active = index == widget.currentTabIndex;
shouldBuildTab[index] = active || shouldBuildTab[index];
return HeroMode(
enabled: active,
child: Offstage(
offstage: !active, // 非アクティブなタブは非表示
child: TickerMode(
enabled: active,
child: FocusScope(
node: tabFocusNodes[index],
child: Builder(builder: (BuildContext context) {
return shouldBuildTab[index] // 一度も表示してないタブはbuildしない
? widget.tabBuilder(context, index) // TabViewを作る
: const SizedBox.shrink(); // プレースホルダー
}),
),
),
),
);
}),
);
}
}
色々と細かい話を省略しましたが、基本的にはNavigator
をOffstage
で包み、Stack
で重ねることで入れ子になったナビゲーションを実現できます。思いの外シンプルですね。これを一般化してまとめたのがnavigator_scopeです。
Flutter公式による実装例
公式ドキュメントにはNavigationBar
を使用した実装例が掲載されています。実装の方針自体は前述のOffstage
とStack
を用いたものと大差ないですが、ブラウザ上で実際に動かせるので一見の価値があると思います。
まとめ
最後に雑ですが個人的フローチャートを書き残しておきます。
- go_routerを使っている 👉
ShellRoute
(状態保存はプルリクエストのマージ待ち) - iOS風のUIを作りたい 👉
CupertinoTabScaffold
とその仲間たち -
Cupertino
は嫌だ 👉 persistent_bottom_nav_bar - とにかくシンプルが良い&好きなタブバーを使いたい 👉 navigator_scope(使ってみてネ)
- 全部嫌だ 👉 自炊🍚
終わり!