BottomNavigatorとは下タブのボタンたちです。色々なアプリで使用されていて、アプリを作る際によく使うと思います。ごく普通に使えば問題なく動作するのですが、TwitterやInstagramのように下タブを隠さないように各タブ内で遷移させようと思うと一捻りする必要があります。
実際にサンプルアプリを作ってみようと思います。
特に指定は無いのでflutter create
で1つ新規でアプリを作ってみてください。
##シンプルなbottomNavigationBar付きのアプリを作る
まずは特に考えないでBottomNavigatonBarを使ったWidgetを作ってみます。
今回はホーム、タイムライン、設定の3タブ構成で作ります。普通に組み合わせて作ると下記のようなコードになると思います。
runApp(); や MaterialAppに関しては省略させていただきます。コード全体を一番下に公開しています。全体像を見たい方は下のリンクからお願いします
import 'package:flutter/material.dart';
import './pages.dart';
class MainPage extends StatefulWidget {
MainPage({Key key}) : super(key: key);
@override
_MainPageState createState() => _MainPageState();
}
class _MainPageState extends State<MainPage> {
int _currentIndex = 0;
final items = <BottomNavigationBarItem>[
BottomNavigationBarItem(
icon: Icon(Icons.home),
title: Text('ホーム'),
),
BottomNavigationBarItem(
icon: Icon(Icons.timeline),
title: Text('タイムライン'),
),
BottomNavigationBarItem(
icon: Icon(Icons.settings),
title: Text('設定'),
),
];
final tabs = <Widget>[
Home(),
Timeline(),
Config(),
];
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('demo'),
),
body: Stack(
children: <Widget>[
IndexedStack(
index: _currentIndex,
children: tabs,
),
],
),
bottomNavigationBar: _buildBttomNavigator(context),
);
}
Widget _buildBttomNavigator(BuildContext context) {
return BottomNavigationBar(
items: items,
currentIndex: _currentIndex,
onTap: (index) {
if (_currentIndex != index) {
setState(() {
_currentIndex = index;
});
}
},
);
}
}
BottomNavigatonBarで切り替わる要素は下記の通りにしておきました。Timeline()、Config()ではクラス名、ボタン内テキストが変わるだけでその他は同じです。
import 'package:flutter/material.dart';
import 'package:./pages.dart';
class Home extends StatelessWidget {
const Home({Key key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Container(
child: Center(
child: FlatButton(
child: Text('Home'),
color: Colors.amberAccent,
onPressed: () {
Navigator.of(context).push<Widget>(
MaterialPageRoute(
builder: (context) {
return Detail();
},
),
);
},
),
),
);
}
}
困ったことにボタンを押して画面が遷移すると下タブが見えなくなってしまいます。
TwitterやInstagramみたいな各タブ内で遷移していくようにしたいんだけどな.........
##問題点
FlutterのNavigatorはスタックで管理させています。
Pushのたびに上に上に画面が乗っていく感じです。Popをすると一番上に乗っているものを取り除いてひとつ下のページが表示されて戻ったことになっています。
つまり遷移するとBottomNavigatonBarがあるWidgetの上に新しいWidgetが乗ってしまいます。
#解決策
Navigatorの中にNavigatorを入れて複数のNavigatorでナビゲーションを管理します。
下記の図に示しているように、tab内で表示されている要素の上に新しいページを乗せて遷移させていく方法で実装しようと思います。
ちなみにタブ内で遷移していますが、ページによってはBottomNavigatonBarを隠して、全画面表示にしたこともあると思いますが問題なく全画面表示にすることができます。
Flutterのsubtree的には下記のようになっています
Navigator
- BottomNavigatorBar
- Navigator
- Widget
- Navigator
- Widget
- Navigator
- Widget
- Navigator
- Widget
この方法しか知らないので○○というWidget使えばいい感じに動くし、簡単に出来るよみたいなことを知っている方がいたらコメント欄で教えて下さい
##Navigator用にタブの数のGlobalKey<NavigatorState>>
を用意する
各タブ内にNavigatorで遷移するときに使用するGlobalKey<NavigatorState>>
を用意します。
タブの数分必要になります。今回はタブの数が3つなので3つ用意します。
class _MainPageState extends State<MainPage> {
TabItem _currentTab = TabItem.home;
Map<TabItem, GlobalKey<NavigatorState>> _navigatorKeys = {
TabItem.home: GlobalKey<NavigatorState>(),
TabItem.timeline: GlobalKey<NavigatorState>(),
TabItem.config: GlobalKey<NavigatorState>(),
};
// 省略
}
TabItemはenumで用意しています。MainPage class の上なり別ファイルなりの適当なところに書いてください。
enum TabItem {
home,
timeline,
config,
}
TabNavigator
Tab内で遷移するのに必要なNavigatorを用意します。
Navigatorには各タブのGlobalKey<NavigatorState>()
が渡り、引数のNavigatorKeyで遷移するようになります。
class TabNavigator extends StatelessWidget {
const TabNavigator({
Key key,
@required this.tabItem,
@required this.routerName,
@required this.navigationKey,
}) : super(key: key);
final TabItem tabItem;
final String routerName;
final GlobalKey<NavigatorState> navigationKey;
Map<String, Widget Function(BuildContext)> _routerBuilder(BuildContext context) => {
'/home': (context) => const Home(),
'/timeline': (context) => const Timeline(),
'/config': (context) => const Config(),
};
@override
Widget build(BuildContext context) {
final routerBuilder = _routerBuilder(context);
return Navigator(
key: navigationKey,
initialRoute: '/',
onGenerateRoute: (settings) {
return MaterialPageRoute<Widget>(
builder: (context) {
return routerBuilder[routerName](context);
},
);
},
);
}
}
BottomNavagtorBar
下タブのBottomNavigatorBarを用意します。
特に変わったことはしていませんが、選択状態の管理をcurrentIndex
でするのではなく、TabItemでしています。
const tabTitle = <TabItem, String>{
TabItem.home: 'ホーム',
TabItem.timeline: 'タイムライン',
TabItem.config: '設定',
};
const tabIcon = <TabItem, IconData>{
TabItem.home: Icons.home,
TabItem.timeline: Icons.timeline,
TabItem.config: Icons.settings,
};
class BottomNavigation extends StatelessWidget {
const BottomNavigation({
Key key,
this.currentTab,
this.onSelect,
}) : super(key: key);
final TabItem currentTab;
final ValueChanged<TabItem> onSelect;
@override
Widget build(BuildContext context) {
return BottomNavigationBar(
items: <BottomNavigationBarItem>[
bottomItem(
context,
tabItem: TabItem.home,
),
bottomItem(
context,
tabItem: TabItem.timeline,
),
bottomItem(
context,
tabItem: TabItem.config,
),
],
type: BottomNavigationBarType.fixed,
onTap: (index) {
onSelect(TabItem.values[index]);
},
);
}
BottomNavigationBarItem bottomItem(
BuildContext context, {
TabItem tabItem,
}) {
final color = currentTab == tabItem ? Colors.blue : Colors.black26;
return BottomNavigationBarItem(
icon: Icon(
tabIcon[tabItem],
color: color,
),
title: Text(
tabTitle[tabItem],
style: TextStyle(
color: color,
),
),
);
}
}
main.dart の body を変更する
IndexedStackで囲っていたWidget を先程作ったTabNavigatorとOffstageで囲みます。
もしタブ内のWidgetにアニメーションがある場合はOffstageの下にTickerModeを入れてみてください。タブが選択されていないときにアニメーションを無効にすることができます。
今回はシンプルに作るために使用しません。
@override
Widget build(BuildContext context) {
return Scaffold(
body: Stack(
children: <Widget>[
_buildTabItem(
TabItem.home,
'/home',
),
_buildTabItem(
TabItem.timeline,
'/timeline',
),
_buildTabItem(
TabItem.config,
'/config',
),
],
),
bottomNavigationBar: BottomNavigation(
currentTab: _currentTab,
onSelect: onSelect,
),
);
}
Widget _buildTabItem(
TabItem tabItem,
String root,
) {
return Offstage(
offstage: _currentTab != tabItem,
child: TabNavigator(
navigationKey: _navigatorKeys[tabItem],
tabItem: tabItem,
routerName: root,
),
);
}
void onSelect(TabItem tabItem) {
if (_currentTab == tabItem) {
_navigatorKeys[tabItem].currentState.popUntil((route) => route.isFirst);
} else {
setState(() {
_currentTab = tabItem;
});
}
}
#Androidの戻るボタンを制御する
WillPopScope Widgetを使ってAndroidの戻るボタンのイベントを検知し処理しています
Timelineタブ、Configタブが選択されているときにAndroidの戻るボタンが押されたときにHomeタブを選択します。
ここの処理はHomeタブを選択しないで選択中のタブの初期ページに戻す等カスタマイズして使ってください。
Future<bool> onWillPop() async {
final isFirstRouteInCurrentTab = !await _navigatorKeys[_currentTab].currentState.maybePop();
if (isFirstRouteInCurrentTab) {
if (_currentTab != TabItem.home) {
onSelect(TabItem.home);
return false;
}
}
return isFirstRouteInCurrentTab;
}
おわり
FlutterのBottomNavigatonBarのNavigatorをいい感じにできたのではないでしょうか?
ちなみに下のgifが今回の完成形の動作です。
今回作ったサンプルのコードをgithubで公開しています。
コード全体が見たい方は下のリンクからどうぞ
今回作ったサンプルアプリ(github)
弊社ではFlutter エンジニアを募集しています。
ご興味がある方は下記のリンクからコーポレートサイトと募集要項など見てみてください。
Flutterエンジニア(コーポレートサイト)