🌶

【Flutter】ボトムナビゲーションバーを表示したまま画面遷移したい【auto_route】

2023/04/27に公開

はじめに

ずっと go_routergo_router_builder を使ってましたが、ボトムナビゲーションバーを表示したまま画面遷移するように ShellRoute を使って修正をしたら、タブを行き来すると画面スタックがクリアされてしまう問題 が go_router では解決できませんでした。そのあたりをサポートしている auto_route に最近乗り換えたので実装方法を紹介します!

https://pub.dev/packages/auto_route

ちなみに、みんな大好きアンドレアさんは Beamer をオススメしてました(試してない)。

https://codewithandrea.com/articles/flutter-bottom-navigation-bar-nested-routes-gorouter-beamer/

https://pub.dev/packages/beamer

こんなことができます

auto_route とは?

タイプセーフな引数の受け渡しとディープリンクを可能にし、コード生成を使用してボイラープレートを最小限にしてくれる Flutter ナビゲーション向けのパッケージです。

主に次の特徴があります。ナビゲーションパッケージに必要な基本的な機能はすべて備わっていると思います。

  • ディープリンク対応
  • コード生成を使用してボイラープレートを最小限にする
  • push()pop() から戻り値を返すことができる
  • ボトムナビゲーションバー、タブバーなどネストされたナビゲーションに対応
  • BuildContext なしでナビゲートができる
  • ユーザー認証状態に応じたリダイレクト(ルートガード)に対応
  • トランジション(画面遷移時のアニメーション)のカスタマイズ

環境

Flutter 3.7.12 • channel stable • https://github.com/flutter/flutter.git
Framework • revision 4d9e56e694 (9 days ago) • 2023-04-17 21:47:46 -0400
Engine • revision 1a65d409c7
Tools • Dart 2.19.6 • DevTools 2.20.1

auto_route の導入方法

基本的には 公式のセットアップと使用法 通りにセットアップしていけばよいんですが、ところどころ誤記などもあるので、ゆーと さんが書いてくれた次の手順も参考にするとよきです!

https://zenn.dev/ncdc/articles/flutter_auto_route

ボトムナビゲーションバーがあるシンプルな画面を実装する

まず、次のように AutoTabsRouter を使ってボトムナビゲーションバーがあるシンプルな画面を実装してみます。

各タブに表示する画面を用意する

各タブに表示する画面 Widget に @RoutePage() アノテーションを追加しておきます。これでホーム画面へ遷移できるようになります。他の各タブ内の画面も同様に追加しておきます。

home_page.dart
+()
class HomePage extends StatelessWidget {
  const HomePage({super.key});

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('ホーム'),
      ),
    );
  }
}

土台となるルートページを用意する

次に、ボトムナビゲーションバーをもつ土台となるルートページを AutoTabsRouter を使い次のように実装します。

タブ押下で tabsRouter.setActiveIndex(index) が実行されてタブ押下イベントが AutoTabsRouter に伝わります。AutoTabsRouter がタブを切り替えると builder(context, child) が発火してタブ内の画面が切り替わります。

root_page.dart
()
class RootPage extends StatelessWidget {
  const RootPage({super.key});

  
  Widget build(BuildContext context) {
    return AutoTabsRouter(
      routes: const [
        // ここに各タブ画面のルートを追加する
        HomeRoute(),
        MypageRoute(),
      ],
      builder: (context, child) {
        // タブが切り替わると発火します
        final tabsRouter = context.tabsRouter;
        return Scaffold(
          body: child,
          bottomNavigationBar: NavigationBar(
            selectedIndex: tabsRouter.activeIndex,
            destinations: const [
              NavigationDestination(
                icon: Icon(Icons.home),
                label: 'ホーム',
              ),
              NavigationDestination(
                icon: Icon(Icons.account_circle),
                label: 'マイページ',
              ),
            ],
            onDestinationSelected: tabsRouter.setActiveIndex,
          ),
        );
      },
    );
  }
}

ルート定義を実装する

最後にルート定義を次のように実装します。初期表示時は / にルーティングされてきます。/ にルーティングされたら RootPage を表示します。RootPage は、さらに 2 つのタブ画面を表示する、という流れです。

app_router.dart
part 'app_router.gr.dart';

@AutoRouterConfig(replaceInRouteName: 'Page,Route')
class AppRouter extends _$AppRouter {
  
  List<AutoRoute> get routes => [
+        AutoRoute(
+          path: '/',
+          page: RootRoute.page,
+          children: [
+            AutoRoute(
+              path: 'home',
+              page: HomeRoute.page,
+            ),
+            AutoRoute(
+              path: 'mypage',
+              page: MypageRoute.page,
+            ),
+          ],
+        ),
      ];
}

これでひとまずボトムナビゲーションバーがある画面が実装できました!

タブ内で画面遷移できるようにする

ホーム画面からお気に入り画面に遷移できるようにして、タブ内で画面遷移をしてみます。
最初にダメなパターンをやってみて、うまくいくように直してみます。

ダメなパターン

実際に私が陥ったダメなパターンで実装してみます。まず、お気に入り画面を新たに用意します。

favorite_page.dart
()
class FavoritePage extends StatelessWidget {
  const FavoritePage({super.key});

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('お気に入り'),
      ),
    );
  }
}

ホーム画面から遷移するので、ホーム画面のルート配下にお気に入り画面のルートを追加します。

app_router.dart
  
  List<AutoRoute> get routes => [
        AutoRoute(
          path: '/',
          page: RootRoute.page,
          children: [
            AutoRoute(
              path: 'home',
              page: HomeRoute.page,
+              children: [
+                AutoRoute(
+                  path: 'favorite',
+                  page: FavoriteRoute.page,
+                ),
+              ],
            ),
            ・・・

ホーム画面にお気に入り画面へ遷移するためのボタンを追加します。これでよさそう!

home_page.dart
  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('ホーム'),
      ),
+      body: ElevatedButton(
+        onPressed: () => context.navigateTo(const FavoriteRoute()),
+        child: const Text('お気に入り'),
+      ),
    );
  }

しかし、動かしてみると次のようにうまくいきません。なぜうまくいかないのでしょうか?

ルーターは孫ルートは検索しない

navigateTo() による画面遷移をするとき、ルート定義内の最も近い親ルーターをみつけ、そのルーターが遷移先のルートを検索します。ルーターが遷移先のルートを検索するときは、孫ルートは検索せず、子ルートまでしか検索してくれません。

ルーターを見つけるロジックについては次で説明されています(ルーターがルートを見つけるロジックについては特に書かれていませんでした)。

https://pub.dev/packages/auto_route#finding-the-right-router

今回の実装のルート定義を図示してみます。ホーム画面からみつかる親ルーターは TabsRouter です。TabsRouter からお気に入り画面は見つかりません。

うまくいくパターン

ホーム画面とお気に入り画面を同じルーターの下にぶら下げるとうまくいきそうです。TabsRouter とホーム画面の間に新しいルーターを追加して、ホーム画面とお気に入り画面を兄弟の関係にしてみます。

新しいルーター HomeRouterRoute を追加します。

home_page.dart
+()
+class HomeRouterPage extends AutoRouter {
+  const HomeRouterPage({super.key});
+}

次のように、RootRouteHomeRoute の間に HomeRouterRoute を挟みます。HomeRouteinitial: true とすることで、/homeHomeRoute にマッピングされます。これでホーム画面とお気に入り画面は同じルーターの下にぶら下がる関係になりました。

app_router.dart
  
  List<AutoRoute> get routes => [
        AutoRoute(
          path: '/',
          page: RootRoute.page,
          children: [
            AutoRoute(
              path: 'home',
-              page: HomeRoute.page,
-              children: [
+              page: HomeRouterRoute.page,
+              children: [
+                AutoRoute(
+                  initial: true,
+                  page: HomeRoute.page,
+                ),
                AutoRoute(
                  path: 'favorite',
                  page: FavoriteRoute.page,
                ),
              ],
            ),
            AutoRoute(
              path: 'mypage',
              page: MypageRoute.page,
            ),
          ],
        ),
      ];

忘れずに AutoTabsRouterroutes も変更しておきます。

root_page.dart
@RoutePage()
class RootPage extends StatelessWidget {
  const RootPage({super.key});

  
  Widget build(BuildContext context) {
    return AutoTabsRouter(
      routes: const [
-        HomeRoute(),
+        HomeRouterRoute(),
        MypageRoute(),
      ],

うまく動きました!タブを行き来しても画面スタックは維持されています 🎉

サンプルコードを公開しています

本記事で紹介したサンプルコードを公開しています!

https://github.com/susatthi/flutter-sample-auto-route-2

もっと auto_route を活用したい

本記事で紹介したシンプルな実装より、より実践に近いサンプルコードも公開しています!是非参考にしてみてください!

  • Riverpod 対応
  • ユーザーのサインイン状態に応じた初期表示画面の出し分け(ガード対応)
  • AutoRouterObserver を使ったログ出力
  • トランジションのカスタマイズ
  • 別のルーター配下の画面への遷移
  • 画面遷移時に画面スタックを自動で積む方法
  • 良い感じのディレクトリ構成

https://github.com/susatthi/flutter-sample-auto-route

最後に

Flutter 大学という Flutter エンジニアに特化した学習コミュニティに所属しています。オンラインでわいわい議論したり、Flutter の最新情報をゲットしたりできます!ぜひ Flutter 界隈を盛り上げていきましょう!

https://flutteruniv.com?invite_id=9hsdZHg0qtaMIr6RPRulAaRJfA83

Discussion