76
59

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

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

Posted at

現状の問題

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

76
59
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
76
59

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?