BottomNavigationBar をキープしたまま画面遷移する

## 現状の問題

`BottomNavigationBar`で表示したWidgetから何も考えずに`Navigator.of(context).push()`で遷移すると以下のように、画面遷移と同時にBottomNavBarが消えてしまう。

![not-keep-bottom-bar.gif (1.8 MB)](https://img.esa.io/uploads/production/attachments/2867/2020/01/08/40525/f71af54e-7ef8-40fe-812d-3b0cf7b14b6a.gif)

表示した画面から遷移するコードは以下のようなイメージ。

```dart

  _buildCategoryButton() {
    return 
      child: RaisedButton(
        child: Row(
          children: [
            Text(
              'カテゴリ名',
              style: TextStyle(
                color: Colors.white,
                fontSize: 15,
                fontWeight: FontWeight.w500,
              ),
            ),
          ],
        ),
        onPressed: () => onPressedCategory(1),
      ),
    );
  }

  void onPressedCategory(int id) {
    Navigator.of(context)
        .push(MaterialPageRoute(builder: (context) => CategoryScreen()));
  }
}

```

画面遷移に伴って`BottomNavigationBar`は消えてほしくないので、頑張ってキープしてみる。

### 各Widget/methodについてちょろっと解説

- `Navigator.of(context)`
    - `The state from the closest instance of this class that encloses the given context.`
    - Flutterのプロジェクトにおいて大体ルートウィジェットとして登録されている`MaterialApp`が持つ`Navigator`を探しに行くメソッド。
- `MaterialPageRoute`はMaterialDesignっぽいアニメーションで遷移するためのウィジェット。

## なぜBottomNavBarは消えるのか

`Navigator.of`は`Navigator`を探しに行くので、BottomNavBarよりも上の`Navigator`を見つけてそれ以下のWidgetをRebuildするため。
WidgetTreeは以下の様になっている。

```
▼ MyApp
 ▼ MaterialApp
  ▼ <some other widgets>
   ▼ Navigator
    ▼ <some other widgets>
     ▼ App
      ▼ Scaffold
       ▼ body: <some other widgets>
       ▼ BottomNavigationBar
```

つまり、`BottomNavigationBar`の祖先ではない`Navigator`を使うことができれば、`BottomNavigationBar`は消えずに画面遷移が出来る。

## CupertinoTab... を使ってpersistな下タブを手に入れろ

あまり紹介されていないのだけれど、`Cupertino`には`CupertinoTab...`という固定された下タブを前提として画面を構成するためのクラス群が提供されている。

↓のcodelabでcupertinoを前提としてアプリを作る記事があり、ここで紹介されている。
https://codelabs.developers.google.com/codelabs/flutter-cupertino/#3

> Cupertino tab has a separate scaffold because on iOS, the bottom tab is commonly persistent above nested routes rather than inside pages.


(iOSデザインに寄せているから下タブがpersistだよ、みたいなことを言っているがMaterial Designのガイドラインでもハッキリ`When used, the bottom navigation bar appears at the bottom of every screen.`と言われている。なんなんだ一体。)

![image.png (149.1 kB)](https://img.esa.io/uploads/production/attachments/2867/2020/01/08/40525/c6194691-8243-4ed8-bd3c-8f3c0e1f33b7.png)

話が逸れたけれども、以下のように実装すると下タブが固定される。

```dart
...

class _MainPageState extends State<MainPage> {
  ...
  
  @override
  Widget build(BuildContext) {
    return CupertinoTabScaffold(
      tabBar: CupertinoTabBar(
        items: const <BottomNavigationBarItem> [
          BottomNavigationBarItem(
            icon: ...
            title ...
          ),
          BottomNavigationBarItem(
            icon: ...
            title ...
          ),
        ],
        onTap: _onItemTapped(index), // 実は無くても動く
        currentIndex: _selectedIndex, // 実は無くても動く
      ),
      tabBuilder: (context, index) {
        switch (index) {
          case 0: // 1番左のタブが選ばれた時の画面
            return CupertinoTabView(builder: (context) {
              return CupertinoPageScaffold(
                navigationBar: CupertinoNavigationBar(
                  leading: Icon(...), // ページのヘッダ左のアイコン
                ),
                child: SearchScreen(), // 表示したい画面のWidget
              );
            });
          case 1: // ほぼ同じなので割愛
            ...
        }
      }
    ),
  },
}
```

### Cupertino周りのクラス1行解説

- [CupertinoTabScaffold](https://api.flutter.dev/flutter/cupertino/CupertinoTabScaffold-class.html)
    - 固定された下タブを持つアプリの構造をscaffoldしてくれるwidget。内部的には↓の`CupertinoTabBar`がタップされたかを勝手にリッスンしてくれるので、自前で`onTap`にコールバックを書かなくても動くのはこいつのおかげ。`tabBar:`で表示するタブの内容を、`tabBuilder:`でTabItemがタップされた時に表示する画面を管理する。
- [CupertinoTabBar](https://api.flutter.dev/flutter/cupertino/CupertinoTabBar-class.html)
    - `BottomNavigationBarItem`を子に持って、下タブを表示する。
- [CupertinoTabView](https://api.flutter.dev/flutter/cupertino/CupertinoTabView-class.html)
    - タブに紐づく1つの画面と、**Navigator**を持つWidget。CupertinoTabViewがNavigatorを持っているというのはミソで、こいつのおかげで各タブごとに画面遷移を戻ったり進んだり出来る。
- [CupertinoPageScaffold](https://api.flutter.dev/flutter/cupertino/CupertinoPageScaffold-class.html)
    - iOSっぽいアプリの構造をscaffoldしてくれるwidget。ぶっちゃけ`CupertinoTabScaffold`の下タブがなくてNavBarの設定ができるやつぐらいにしか知りません。
- [CupertinoNavigationBar](https://api.flutter.dev/flutter/cupertino/CupertinoNavigationBar-class.html)
    - ↑のCupertinoPageScaffoldの`navigationBar:`に入るwidget。leadingにアイコンを設定すると勝手に戻ったりしてくれる偉い子。

### CupertinoPageScaffold.child で呼ばれるWidgetもCupertinoPageScaffoldしなきゃいけなかったりするの？

いいえ。タブ内に表示される画面のルートWidgetはシンプルな`Scaffold`で問題ありません。以下のような感じです。

```dart
class SearchScreen extends StatefulWidget {
  SearchScreen({Key key}) : super(key: key);

  @override
  _SearchScreenState createState() => _SearchScreenState();
}

class _SearchScreenState extends State<SearchScreen> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('ギフトを探す'),
      ),
      body: Container(
        ...
      )
    )
  }
}

```

何のことはない、普通のStatefullWidgetで大丈夫です。

## 最終的に出来上がるもの

ドン。完璧ですね。

以下のgifを見れば分かる通り、一度画面遷移をした後に、別のタブに切り替えから再度最初のタブに切り替えても画面遷移がそのまま保持されていますね。

![aaa.gif (4.8 MB)](https://img.esa.io/uploads/production/attachments/2867/2020/01/08/40525/9bb0aaee-a24d-479b-8346-1d9cb07baa61.gif)

無事に下タブを保持したまま画面遷移できました。
お疲れ様でした。

## その他

- …書き終わってから気づいたのですが、標準のBottomNavigationBarだと各タブをタップした際に、タップされたアイコンが大きくなったり微妙にアニメーションしたりするマイクロインタラクションが入ってるのですが、Cupertinoだとシンプルに切り替わってるだけになってます。どうしても標準のBottomNavigationBarを使いたい場合、CupertinoScaffoldが実装してることを自前で書く方法があるのでこちらで対処しましょう。
    - https://medium.com/coding-with-flutter/flutter-case-study-multiple-navigators-with-bottomnavigationbar-90eb6caa6dbf


### 本文中に載らなかった参考URL

- おじさんがCupertinoTabScaffoldでコーディングしてる動画
  - https://www.youtube.com/watch?v=xb3px1sT26E