flutterの勉強をしていて、Bottom Navigationについて相当苦しんだので共有しようと思います。
Bottom Navigationといえば上の画像にもあるようにいわゆる「下タブ」です。
instagramやfacebookなど、多くのアプリがこの形式をとっており、自分自身でアプリを作ろうと思った際にもとりあえずBottom Navigationで、ということも少なくないのではないでしょうか。ハンバーガーボタンと比較して画面遷移がシームレスだったりモーダル画面作らなくてよかったり、UI的にも開発的にも優しいように見えます。
当然、flutterもこの辺のBottom Navigationはデフォルトで備えています。では何がそんなにハマるのでしょうか。
パターン1: pageControllerで頑張る
1つ目の方法としては、Bottom Navigationを呼び出して、PageControllerを使ってページの操作を行うものです。
まずサンプルのページを用意します。これはほぼインストール時そのままのページを使ってます(実際なんでもOKです)
class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);
final String title;
@override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
int _counter = 0;
void _incrementCounter() {
setState(() {
_counter++;
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(
'You have pushed the button this many times:',
),
Text(
'$_counter',
style: Theme.of(context).textTheme.display1,
),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: _incrementCounter,
tooltip: 'Increment',
child: Icon(Icons.add),
),
);
}
}
このページを呼び出すようにBottom Navigationを作成します。
class BasePage extends StatefulWidget {
@override
_BasePageState createState() => _BasePageState();
}
class _BasePageState extends State<BasePage> {
// page index
int _index = 0;
PageController _pageController;
@override
void initState() {
super.initState();
_pageController = new PageController();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('My Flutter App'),
),
body: new PageView(
controller: _pageController,
onPageChanged: (int index) {
setState(() {
this._index = index;
});
},
children: [
MyHomePage(title: "page 1"), //call pages
MyHomePage(title: "page 2"),
MyHomePage(title: "page 3"),
]),
bottomNavigationBar: BottomNavigationBar(
onTap: (int index) { // define animation
_pageController.animateToPage(index,
duration: const Duration(milliseconds: 10), curve: Curves.ease);
},
currentIndex: _index,
items: [
BottomNavigationBarItem( // call each bottom item
icon: new Icon(Icons.home),
title: new Text('Home'),
),
BottomNavigationBarItem(
icon: new Icon(Icons.mail),
title: new Text('Messages'),
),
BottomNavigationBarItem(
icon: Icon(Icons.person), title: Text('Profile'))
],
),
);
}
}
どうでしょう。pageControllerを呼び出すことでページの遷移を定義すればこのように簡単に
Bottom Navigationを利用できます。しかもページ間スワイプで移動もできます。
この書き方の長所としては、作成が簡単で、ページ間をスワイプ移動できるメリットがあります。しかし一方でページを遷移するたびにいちいち初期化するために、値を保持したり裏で計算を別のページで回すといったことができません(サンプルだと+ボタンで数字をインクリメントしてもページ移動するとリセットされることが分かります)
さて、どうしましょう
パターン2: Stackで重ねる
さて、パターン1では毎回ページが破棄されることが問題でした。ではページを破棄されないように持っておけばいいじゃないかというのが2つ目の考え方になります。
class _BasePageState extends State<BasePage> {
int _index = 0;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('My Flutter App'),
),
body: new Stack(
children: <Widget>[
_buildOffstage(0, MyHomePage(title: "page 1")),
_buildOffstage(1, MyHomePage(title: "page 2")),
_buildOffstage(2, MyHomePage(title: "page 3")),
],
),
bottomNavigationBar: BottomNavigationBar(
onTap: (int index) {
setState(() {
this._index = index;
});
},
currentIndex: _index,
items: [
BottomNavigationBarItem(
icon: new Icon(Icons.home),
title: new Text('Home'),
),
BottomNavigationBarItem(
icon: new Icon(Icons.mail),
title: new Text('Messages'),
),
BottomNavigationBarItem(
icon: Icon(Icons.person), title: Text('Profile'))
],
),
);
}
// onstage view is only visible
Widget _buildOffstage(int index, Widget page) {
return Offstage(
offstage: index != _index,
child: new TickerMode(
enabled: index == _index,
child: page,
),
);
}
}
上のコードではpageControllerを使わなくなった代わりにOffStageを使い、選択されているView以外は非表示にする処理を加えました。これにより、タップして下画面のページを切り替えてもページが消去されないため、インクリメントした値が引き継がれるのが分かるかと思います。
この書き方をするとページ間をスワイプ移動できなくなります。しかし、ページの間を移っても状態が保持されるのでページをまたいで値を保持したい場合には便利です。
パターン2の問題点
さて、パターン2だと何が問題になるでしょうか。ページ遷移を行った時にBottom Navigationが消えてしまうという問題が発生します。じゃあ遷移先にBottom Navigationつければいいじゃん!!ってなるかもしれませんがそうはうまくいきません。アニメーションを入れるとBottom Navigationごとスワイプしてしまいます。
これらの話は長くなりそうなので次回に回せればと思います。