flutterのBottom Navigationで苦しんだ話 その1

flutterの勉強をしていて、Bottom Navigationについて相当苦しんだので共有しようと思います。

<img width="353" alt="スクリーンショット 2018-12-03 22.09.20.png" src="https://qiita-image-store.s3.amazonaws.com/0/180185/fa81827e-bfa5-7ea0-e09d-d88c155357a4.png">

Bottom Navigationといえば上の画像にもあるようにいわゆる「下タブ」です。
instagramやfacebookなど、多くのアプリがこの形式をとっており、自分自身でアプリを作ろうと思った際にもとりあえずBottom Navigationで、ということも少なくないのではないでしょうか。ハンバーガーボタンと比較して画面遷移がシームレスだったりモーダル画面作らなくてよかったり、UI的にも開発的にも優しいように見えます。

当然、flutterもこの辺のBottom Navigationはデフォルトで備えています。では何がそんなにハマるのでしょうか。

## パターン1： pageControllerで頑張る
１つ目の方法としては、Bottom Navigationを呼び出して、PageControllerを使ってページの操作を行うものです。

まずサンプルのページを用意します。これはほぼインストール時そのままのページを使ってます(実際なんでもOKです)

```dart
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を作成します。

```dart

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つ目の考え方になります。

```dart
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ごとスワイプしてしまいます。

これらの話は長くなりそうなので次回に回せればと思います。