8
6

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 1 year has passed since last update.

BottomNavigationBarを固定したまま画面遷移する方法をまとめてみる

Last updated at Posted at 2023-04-27

iOS版のTwitterなどで使われているお馴染みのUIです。ググれば様々な実装方法が出てきますが、たくさんあって混乱するので一度ここにまとめたいと思います。

twitter

1. CupertinoTab系のWidgetを使う

FlutterにはiOS風のUIを再現するためにCupertino*と名のつくWidget群が用意されており、その中のCupertinoTabScaffoldCupertinoTabBar等を使うと固定タブバーが実現できます。この方法については以下の記事が詳しいです。

また公式ドキュメントにはブラウザで動かせるインタラクティブな実装例が掲載されているのでオススメです。

cupertino

ただしこの方法だとCupertinoTabBar以外のタブバーが使えません。BottomNavigationBaranimated_bottom_navigation_barcurved_navigation_barのようなサードパーティー製のタブバーなどを使用したい場合は他の手法を選択する必要があります。

2. persistent_bottom_nav_barを使う

読んで字の如しなパッケージがあります。タブバーの見た目にはいくつか選択肢があり、かなり柔軟に使用できそうです。また拡張することで自分好みのスタイルも作ることができるようです。

persistentbottomnavbar

3. go_routerを使う

go_routerNavigator2.0を簡単に利用できるルーティングライブラリです。go_routerではShellRouteによる階層的なナビゲーションもサポートされており、これにより固定タブバーの永続化が実現できます。日本語記事では

あたりが分かりやすいかと思います。公式にもサンプルが用意されているため、go_routerをすでに使用している場合はこれを利用するのが一番楽かもしれません。

ただし、現状ShellRouteは各タブの状態を保持してくれません。例えばホームタブで作業したあと検索タブに遷移し、その後ホームタブに再び戻ると作業状態がリセットされてしまいます。

shellroute

この問題についてはすでにGitHub上でissueが挙がっており、現在はプルリクエストのマージ待ちのようです。

追記(2023/5/27)

マージされたようですね。v7.1から使えるそうです。

4. navigator_scopeを使う

宣伝です。このために記事を書いたようなものです。前述のCupertinoTab*で実装されている入れ子のナビゲーション機能を抜き出し、より一般化してパッケージにまとめました。

とにかくシンプルなことが売りです。Navigator2.0は複雑すぎ、Navigation.pushしてNavigation.popするだけで十分、という場合におすすめです。新しいAPIを学ぶ必要もありません。またBottomNavigationBarだけでなく、NavigationBarNavigationDrawer(横から飛び出すメニュー)、さらにはanimated_bottom_navigation_barcurved_navigation_barなどの著名なサードパーティー製のタブバーも自由に利用できます。

navigatorscope

使い方は簡単で、NavigatorScopeの中で各タブのNestedNavigatorを作るだけです。もちろん画面遷移の前後で各タブの状態は保持されます。

Scaffold(
  bottomNavigationBar: NavigationBar( // お好きなタブバーをひとつ
    selectedIndex: selectedTabIndex,
    destinations: tabs,
    onDestinationSelected: (index) {
        setState(() => selectedTabIndex = index);
    },
    ...
  ),
  body: NavigatorScope( // ローカルなNavigatorのハブとなるWidget
    currentDestination: selectedTabIndex, // 現在アクティブなタブのインデックス
    destinationCount: tabs.length,
    destinationBuilder: (context, index) {
      return NestedNavigator( // ローカルなNavigator
        // このNavigatorのデフォルトのページを作成する
        builder: (context) => Container(),
      );
    },
  ),
);

タブを切り替えるときはcurrentDestinationの値を変えてNavigatorScopeをrebuildするだけです。タブ内で画面遷移する場合は通常通りNavigator.pushNavigator.popを使用します。詳しくはREADMEをご覧ください。

5. 自前で実装する

最終手段です。Deep linkingやらNavigator2.0やらを考慮しだすと複雑になりますが、基本的な部分だけを抜き出してみると案外簡単に実装できます。

CupertinoTab*の実装を理解する

前述の通り、CupertinoTab*系のWidget群はデフォルトで固定タブバーの機能を提供しています。特に重要なのはCupertinoTabViewCupertinoTabScaffoldで、前者はタブのNavigatorを内部で保持し、後者は各タブの切り替えとそのNavigatorの状態保持を担当しています。Widgetツリーのイメージとしては下のような感じです。

CupertinoTabScaffold
  └ _TabSwitchingView
      ├ CupertinoTabView
      │   └ Navigator
      ├ CupertinoTabView
      ...

_TabSwitchingViewCupertinoTabScaffold内で使われているプライベートなクラスで、Navigatorの切り替えや状態保持を実質的に担当しているWidgetです。またこれらは全てStatefulWidgetとして定義されているので、以後「CupertinoTabViewbuildメソッド」のような表記がされていた場合は「対応するStateクラスのbuildメソッド」と読み替えてください。

CupertinoTabView

細かい話を抜きにすれば、CupertinoTabViewがしていることはNavigatorを1つ保持する、ただそれだけです。実際buildメソッドは以下のようにとてもシンプルな実装です(ソースコード)。

// _CupertinoTabViewState
  @override
  Widget build(BuildContext context) {
    return Navigator(
      key: widget.navigatorKey,
      onGenerateRoute: _onGenerateRoute,
      onUnknownRoute: _onUnknownRoute,
      observers: _navigatorObservers,
      restorationScopeId: widget.restorationScopeId,
    );
  }

初期ページを作成したり新しいページがNavigator.pushNamedされたりするとonGenerateRouteが呼ばれます。ここで作成されたRoute(デフォルトではCupertinoPageRoute)がページスタックに積まれることになるのですが、今回は関係ないので割愛します。

CupertinoTabScafold

各タブのNavigatorCupertinoTabView)はCupertinoTabScaffold内の_TabSwitchingViewが管理しています。CupertinoTabScaffoldbuildメソッドを見てみましょう(ソースコード)。重要な部分以外は省略しました。_controller.indexが現在アクティブなタブのインデックスを表しており、これが変わるとsetStateでrebuildが走るように実装されています。また各Nvigatorwidget.tabBuilderを通して_TabSwitchingViewに渡されます。

// _CupertinoTabScaffoldState
@override
  Widget build(BuildContext context) {
    ...
    Widget content = _TabSwitchingView(
      currentTabIndex: _controller.index, // これが変わるとrebuild
      tabCount: widget.tabBar.items.length,
      tabBuilder: widget.tabBuilder, // 各タブのCupertinoTabViewを作成するbuilder
    );
    ...
    return DecoratedBox(
      ...
      child: Stack(
        children: <Widget>[
          content,
          ...
        ],
      ),
    );
  }

最後に_TabSwitchingViewbuildメソッドも見てみましょう(ソースコード)。こちらも至ってシンプルです。ネストが少々深いですが、要点は「前述のtabBuilderが作るCupertinoTabViewOffstageで囲み、Stackで重ねる」ことです。Offstageを使うのは非アクティブなタブを非表示にするためです。HeroModeTickerModeにも囲まれていますが、これは非表示タブのアニメーションを無効化するためです。親のCupertinoTabScaffoldに巻き込まれてrebuildされると各タブのactive = index == widget.currentTabIndex;の値が更新され、タブが切り替わります。

// _TabSwitchingViewState
@override
  Widget build(BuildContext context) {
    return Stack(
      fit: StackFit.expand,
      children: List<Widget>.generate(widget.tabCount, (int index) {
        // 各タブがアクティブかどうか?
        final bool active = index == widget.currentTabIndex;
        shouldBuildTab[index] = active || shouldBuildTab[index];

        return HeroMode(
          enabled: active,
          child: Offstage(
            offstage: !active, // 非アクティブなタブは非表示
            child: TickerMode(
              enabled: active,
              child: FocusScope(
                node: tabFocusNodes[index],
                child: Builder(builder: (BuildContext context) {
                  return shouldBuildTab[index] // 一度も表示してないタブはbuildしない
                    ? widget.tabBuilder(context, index) // TabViewを作る
                    : const SizedBox.shrink(); // プレースホルダー
                }),
              ),
            ),
          ),
        );
      }),
    );
  }
}

色々と細かい話を省略しましたが、基本的にはNavigatorOffstageで包み、Stackで重ねることで入れ子になったナビゲーションを実現できます。思いの外シンプルですね。これを一般化してまとめたのがnavigator_scopeです。

Flutter公式による実装例

公式ドキュメントにはNavigationBarを使用した実装例が掲載されています。実装の方針自体は前述のOffstageStackを用いたものと大差ないですが、ブラウザ上で実際に動かせるので一見の価値があると思います。

まとめ

最後に雑ですが個人的フローチャートを書き残しておきます。

  • go_routerを使っている 👉 ShellRoute(状態保存はプルリクエストのマージ待ち)
  • iOS風のUIを作りたい 👉 CupertinoTabScaffoldとその仲間たち
  • Cupertinoは嫌だ 👉 persistent_bottom_nav_bar
  • とにかくシンプルが良い&好きなタブバーを使いたい 👉 navigator_scope(使ってみてネ)
  • 全部嫌だ 👉 自炊🍚

終わり!

8
6
0

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
8
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?