前置き
FlutterのBottomNavigationBarを使って、タブ間遷移を実現する方法に四苦八苦しました。
その実装方法を共有します。
下記画像の様にタブA-画面1からタブB-画面2へ遷移したい場合ってありませんか?
また、タブB-画面2から戻る際には、タブA-画面1では無くタブB-画面1に戻りたいと考えました。
本題
Pages APIをstate_notifierを使って実装しました。
Pages APIにの使用方法については、下記の記事が参考にさせて頂きました。
state_notifierの使用方法については、下記の記事を参考にさせて頂きました。
それでは、実装方法をざっくり説明します。
Stateの定義
state_notifierで保持するState(状態)となるクラスを定義します。
プロパティとして持つのは下記項目です。
- 現在表示しているタブ
- タブに表示しているページ一覧
実装イメージは下記の通りです。
enum BottomTab {
タブ1,
タブ2,
}
@freezed
class RouteState with _$RouteState {
const factory RouteState({
required BottomTab 選択中のタブ,
@Default([]) List<Page> タブ1ページ一覧,
@Default([]) List<Page> タブ2ページ一覧,
}) = _RouteState;
}
実際の実装サンプルはこちらです。
Stateの値は下記の様に変化します。
初期状態(タブ1-画面1が表示される)
プロパティ名 | プロパティ値 |
---|---|
選択中タブ | タブ1 |
タブ1ページ一覧 | [] |
タブ2ページ一覧 | [] |
タブ2を押下(タブ2-画面1が表示される)
プロパティ名 | プロパティ値 |
---|---|
選択中タブ | タブ2 |
タブ1ページ一覧 | [] |
タブ2ページ一覧 | [] |
画面2を押下(タブ2-画面2が表示される)
プロパティ名 | プロパティ値 |
---|---|
選択中タブ | タブ2 |
タブ1ページ一覧 | [] |
タブ2ページ一覧 | [タブ2-画面2] |
Notifierの定義
Stateを更新するNotifierを下記の通り定義します。
class RouteStateNotifier extends StateNotifier<RouteState> with LocatorMixin {
RouteStateNotifier() : super(const RouteState(tab: initialTab));
Future changeIndex(BottomTab tab) async {
// 選択中のタブを切り替える
}
Future push(BottomTab tab, Page page) async {
// ページ一覧の末尾にページを追加する
}
Future pop(BottomTab tab) async {
// ページ一覧の末尾のページを取り除く
}
Future replace(BottomTab tab, List<Page> pages) async {
// ページ一覧のまるっと入れ替える
}
}
Navigatorの定義
Stateの保存されているページ一覧を画面に表示する為、Navigatorのpagesに渡します。
class ProjectsNavigator extends StatelessWidget {
@override
Widget build(BuildContext context) {
final pages = context.select((RouteState state) => state.タブ2ページ一覧);
return Navigator(
pages: [
タブ2-画面1(divisionId: divisionId!, openDrawer: _openDrawer),
...pages
],
);
}
}
BottomNavigationBarの実装
BottomNavigationBarとStateを繋ぎます。
class MainScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
final tab = context.select((RouteState state) => state.選択中のタブ);
final index = tab.getIndex();
return WillPopScope(
child: Scaffold(
body: IndexedStack(
index: index,
children: _children,
),
bottomNavigationBar: BottomNavigationBar(
currentIndex: index,
items: _tabItems,
onTap: (int index) {
final tab = BottomTabExt.getTab(index);
final notifier = context.read<RouteStateNotifier>();
if (tab != context.read<RouteState>().tab) {
notifier.changeIndex(tab);
}
},
),
),
);
}
}
画面遷移の実装
後は、Notifierを通じてStateを更新することで画面遷移が実現できます。
class HomeScreen extends StatefulWidget {
@override
_HomeScreenState createState() => _HomeScreenState();
}
class _HomeScreenState extends State<HomeScreen> {
Future _onPressedProjectDetail() async {
final notifier = context.read<RouteStateNotifier>();
await notifier.changeIndex(BottomTab.タブ2);
await notifier.replace(BottomTab.タブ2, [
タブ2-画面2(),
]);
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
ElevatedButton(
onPressed: _onPressedProjectDetail,
child: const Text('タブ2-画面2へ'),
),
],
),
),
);
}
}
備考
その他、画面遷移でこだわった点のメモです。
この辺の実装サンプルは、下記クラスに集約されています。
タブの状態保持
IndexedStackを使ってタブの状態を保持しています。
ただし、普通に実装してしまうと親がビルドされるタイミングで子のタブ全てがビルドされてしまいます。
そこで、初期状態はLoadingScreenを保持し、タブが呼ばれたタイミングで本命のタブ(ProjectsNavigator)に入れ替えるということをしています。
選択中のタブ押下
選択中のタブを押下することで、先頭の画面に戻るようにしています。
(例: タブ2-画面2を表示している状態で、タブ2が押下されるとタブ2-画面1に戻ります。)
Androidの戻るボタン対応
WillPopScopeを使って戻るボタンを押下された際、下記の様に動作するようにしています。
- Drawerが表示されている状態の場合、Drawerを閉じる。
- タブ2-画面2が表示されている場合、タブ2-画面1に戻る。
- タブ2-画面1が表示されている場合、タブ1-画面1に戻る。
- タブ1-画面1が表示されている場合、アプリを閉じる。
横スワイプ対応
タブ1-画面1やタブ2-画面1などタブ内の先頭の画面で横スワイプした場合は、Drawerを表示したいですが、タブ2-画面2など遷移先にいる場合は、横スワイプで前の画面に戻りたかったりします。
その為、drawerEnableOpenDragGesture
というプロパティをStateに合わせて制御しています。