42

この記事は最終更新日から1年以上が経過しています。

@canisterism

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

現状の問題

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

not-keep-bottom-bar.gif (1.8 MB)

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


  _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.ofNavigatorを探しに行くので、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)

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

...

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

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

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

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)

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

その他

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

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

ユーザー登録して、Qiitaをもっと便利に使ってみませんか。
  1. あなたにマッチした記事をお届けします
    ユーザーやタグをフォローすることで、あなたが興味を持つ技術分野の情報をまとめてキャッチアップできます
  2. 便利な情報をあとで効率的に読み返せます
    気に入った記事を「ストック」することで、あとからすぐに検索できます
canisterism
Rails, Vue, React, Elm, Flutterなどがちょっとだけ書けます
giftee
giftee (株式会社ギフティ) は、ソーシャルギフトサービス 「giftee」、法人向けデジタルギフトチケット販売画面の提供、その他O2Oソリューションなどを展開する五反田のスタートアップです。(onlab第1期, KDDI ∞ LABO 第1期)
この記事は以下の記事からリンクされています

コメント

(編集済み)

こちらのコードを参考に作成したコードについて、
自分の環境では tabBuilder の部分でワーニングが消えませんでした。

環境は 以下です。
AndroidStudio 4.0.1
Flutter 1.25.0-8.1.pre
Dart 2.12.0 (build 2.12.0-133.2.beta)

ワーニングが出るのは以下ソース(本文コピペ)の switch 文のところです

...

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: // ほぼ同じなので割愛
            ...
        }
      }
    ),
  },
}

tabBuilder の中で switch を使っているため、コード上で各case内で網羅的に CupertinoPageScaffold を return しても
コンパイラが return が確実にあることを解釈できないように見えます。
公式の記事 を参考にしたところ以下であればワーニングも出ずこちらのほうが良さそうですがいかがでしょうか。

...

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) {
      //// 返り値のための変数作成
      CupertinoTabView returnView;
        switch (index) {
          case 0: // 1番左のタブが選ばれた時の画面
            returnView = CupertinoTabView(builder: (context) {
              return CupertinoPageScaffold(
                navigationBar: CupertinoNavigationBar(
                  leading: Icon(...), // ページのヘッダ左のアイコン
                ),
                child: SearchScreen(), // 表示したい画面のWidget
              );
            });
            break; //// break 文も必要そうです
          case 1: // ほぼ同じなので割愛
            ...
        }
      }
      return returnView; //// switch 文を抜けて tabBuilder の最後に return する  
    ),
  },
}

よろしくおねがいします。

0
あなたもコメントしてみませんか :)
ユーザー登録
すでにアカウントを持っている方はログイン
記事投稿イベント開催中
自社サービスの技術スタック公開
~
Docker上のみでシステムを作るときの構成
~