やりたいこと
iOSのFacebookアプリのように、BottomNavigationのタップでページ切り替えやリフレッシュを行いたい。
- 詳細画面は、タブを表示したまま遷移したい。
- 他タブから戻ってきたときには、以前の状態のまま表示したい。
- 詳細画面表示中に選択中のタブを押された場合、タブの先頭画面を表示したい。
- タブの先頭画面表示中に選択中のタブを押された場合、タブの先頭画面をリロードしたい。
やったことの概要
1.や2.を実現するため、 CupertinoTabScaffold
を利用します。
(materialに含まれる BottomNavigationBar
などでは、下部タブは残りません。)
3.と4.を実現するために、いくつか処理を実装しました。
※ 状態の保持・処理の伝搬には state_notifier を使っています。(後述)
コードの全体は https://github.com/noboru-i/flutter_sample/commit/1308f35413d372a987f88822aa6403f25ed5bcb0 のあたりです。
完成動作イメージ
HomePage表示
-> アカウントタブ選択でAccountPage表示
-> ホームタブ選択でHomePageに戻る(リストの数字は変わらない)
-> ページ内ボタンタップでHomeDetailに遷移
-> ホームタブタップでHomePageに戻る
-> 再度ホームタブタップで画面がリロードされる(リストの数字が変わる)
実装の内容
タブの先頭画面に戻す
CupertinoTabScaffold
の tabBuilder
で返却する CupertinoTabView
にて、生成しておいた navigatorKey
を渡します。
(そのため、 CupertinoTabScaffold
を内包するWidgetはStatefulWidgetにしておきます)
また、 CupertinoTabScaffold
にも CupertinoTabController
を設定しておきます。
CupertinoTabBar
の onTap
のタイミングで、
_tabNavKeyList[_controller.index].currentState.popUntil((route) => route.isFirst);
のように、先頭までpopします。
抜粋すると、こんな感じです。
class _BottomTabScreenState extends State<BottomTabScreen> {
final List<GlobalKey<NavigatorState>> _tabNavKeyList =
List.generate(2, (index) => index)
.map((e) => GlobalKey<NavigatorState>())
.toList();
final CupertinoTabController _controller = CupertinoTabController();
int _oldIndex = 0;
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return StateNotifierProvider<BottomTabStateNotifier, BottomTabState>(
create: (_) => BottomTabStateNotifier(),
builder: (context, snapshot) {
return WillPopScope(
onWillPop: () async {
return !await _tabNavKeyList[_controller.index]
.currentState
.maybePop();
},
child: CupertinoTabScaffold(
controller: _controller,
tabBar: CupertinoTabBar(
onTap: (index) => _onTapItem(context, index),
items: <BottomNavigationBarItem>[
// ...return any bar items.
],
),
tabBuilder: (BuildContext context, int index) {
return CupertinoTabView(
navigatorKey: _tabNavKeyList[index],
builder: (BuildContext context) {
// ...return any widgets
},
);
},
),
);
},
);
}
void _onTapItem(BuildContext context, int index) {
if (index != _oldIndex) {
_oldIndex = index;
return;
}
_tabNavKeyList[_controller.index].currentState.popUntil((route) => route.isFirst);
}
}
タブの先頭画面をリロード
API実行や情報の保持は state_notifier
を利用していました。
そのため、タブがリロードを意図して押されたことを伝搬するためにも、 state_notifier
の仕組みを利用しました。1
まず、「タブ全体画面」用のStateNotifier (=BottomTabStateNotifier
)と、「タブの中の画面」用のStateNotifier (=HomeStateNotifier
)を用意しました。
BottomTabStateNotifier
の中で Map<TabItem, RefreshListener> listeners = {};
という変数を定義して、そこに外部から追加・削除をできるようにしました。2
class BottomTabStateNotifier extends StateNotifier<BottomTabState> {
BottomTabStateNotifier() : super(BottomTabState());
Map<TabItem, RefreshListener> listeners = {};
void addRefreshListener(TabItem item, RefreshListener listener) {
listeners[item] = listener;
}
void removeRefreshListener(TabItem item) {
listeners.remove(item);
}
}
enum TabItem {
home,
account,
}
typedef RefreshListener = void Function();
また、先程の _onTapItem
から実行され、登録されたListenerに伝搬させるためのメソッドを追加しました。
void _onTapItem(BuildContext context, int index) {
if (index != _oldIndex) {
_oldIndex = index;
return;
}
final canPop = _tabNavKeyList[_controller.index].currentState.canPop();
if (canPop) {
_tabNavKeyList[_controller.index]
.currentState
.popUntil((route) => route.isFirst);
} else {
context.read<BottomTabStateNotifier>().notifyRefresh(_tabTypes[index]);
}
}
class BottomTabStateNotifier extends StateNotifier<BottomTabState> {
// ...omit
void notifyRefresh(TabItem item) {
listeners[item]?.call();
}
}
canPop
で判定することで、先頭ページと、それ以外のページの動作を分けることができています。
あとは、イベントを受け取る側の HomeStateNotifier
を実装します。
class HomeStateNotifier extends StateNotifier<HomeState> with LocatorMixin {
HomeStateNotifier() : super(HomeState()) {
refresh();
}
@override
void initState() {
read<BottomTabStateNotifier>().addRefreshListener(
TabItem.home,
() => refresh(),
);
}
@override
void dispose() {
read<BottomTabStateNotifier>().removeRefreshListener(TabItem.home);
super.dispose();
}
Future<void> refresh() async {
// ...something API calling
}
initState
のタイミングでListener登録しておきます。また、 dispose
のタイミングで削除しておきます。
あとは、 refresh
でstateを変更して、それをUI側で処理してやれば、タブのタップで画面のリロードが実現できます。
参考にしたもの
BottomNavigationBar をキープしたまま画面遷移する - Qiita
[Flutter] Tab内で遷移したPageをAndroid Hardware Back Buttonで閉じる - Qiita
https://stackoverflow.com/a/62018741/4531070