🤺

GoRouterの新機能を試してみる

2023/07/01に公開

進化するのが早過ぎる🤦

GoRouterが最近バージョンアップして、以前よりも安定していることやStatefulShellRouteなるものが追加されたので、試してみました。気が付かない内にどんどん新しい機能が追加されていく!

✅こちらが全体のコードです

https://github.com/sakurakotubaki/StatefulShellRouteRiverpod

使用したパッケージ

flutter_riverpod: ^2.3.6
go_router: ^9.0.0

👁️海外の記事を参考にやってみた

flutter_riverpod2.0を使って、動作するか今回試してみました。元のコードは状態管理はされていません。ボトムナビゲーションバーで、画面遷移するページも追加してみました。

https://medium.com/@antonio.tioypedro1234/flutter-go-router-the-essential-guide-349ef39ec5b3

今回使った機能はこちら
https://pub.dev/documentation/go_router/latest/go_router/StatefulShellRoute-class.html

StatefulShellRouteクラス

英語を翻訳しております
UIシェルを表示するルートで、そのサブルートには個別のナビゲータがあります。

ShellRouteと同様に、このルートクラスはルートNavigatorとは異なるNavigatorにサブルートを配置します。しかし、このルートクラスはネストされたブランチ(つまり並列ナビゲーションツリー)ごとに別々のナビゲータを作成する点が異なり、ステートフルなネストされたナビゲーションを持つアプリを構築することができます。これは、例えばBottomNavigationBarを持つUIを実装し、各タブの永続的なナビゲーション状態を持つ場合に便利です。

StatefulShellRouteは、StatefulShellBranchアイテムのリストを指定することで作成されます。StatefulShellBranchは、ルートルートとブランチのナビゲータキー(GlobalKey)、およびオプションの初期位置を提供します。

ShellRouteと同様に、StatefulShellRouteを作成する際にはビルダーかpageBuilderのどちらかを提供する必要があります。ただし、これらのビルダーは子ウィジェットの代わりに StatefulNavigationShell パラメータを受け付ける点が若干異なります。StatefulNavigationShell は、ルートの状態に関する情報にアクセスしたり、アクティブなブランチを切り替える(別のブランチのナビゲーションスタックを復元する)ために使用できます。後者は、例えばStatefulNavigationShell.goBranchメソッドを使用することで実現できます:

void _onItemTapped(int index) {
  navigationShell.goBranch(index: index);
}

StatefulNavigationShellは、ブランチナビゲータの状態を管理・維持する役割も担っています。通常、シェルはこのWidgetを中心に構築されます。例えば、BottomNavigationBarを持つScaffoldのボディとして使用します。

StatefulShellRouteを作成する際には、navigatorContainerBuilder関数を提供する必要があります。この関数は、ブランチ・ナビゲータを表すウィジェットの実際のコンテナを構築します。通常、この関数によって返されるウィジェットは、ブランチ・ナビゲータのレイアウト(オフステージの処理などを含む)と、アクティブなブランチを切り替えるときに必要なアニメーションを処理します。

ほとんどのユースケースに適したnavigatorContainerBuilderのデフォルト実装としては、コンストラクタStatefulShellRoute.indexedStackの使用を検討してください。

StatefulShellRoute(およびそれ以下のルート)では、同じナビゲーションスタック内のルート間のアニメーション遷移は他のルートクラスと同じように動作し、pageBuilderを使用してカスタマイズできます。ただし、StatefulShellRouteは並列ナビゲーションスタックのセットを保持するため、ブランチ間の切り替え時のトランジションはブランチナビゲータコンテナ(つまりnavigatorContainerBuilder)の責任となります。デフォルトのIndexedStack実装(StatefulShellRoute.indexedStack)では、アニメーション遷移は使用しませんが、これを実現する方法の例が提供されています(以下のStatefulShellRouteのカスタム例へのリンクを参照)。

こちらも参照のこと:

StatefulShellRoute.indexedStackは、ほとんどのユースケースに適したデフォルトのStatefulShellRoute実装を提供します。
StatefulShellRouteを使用した実行可能な完全な例については、Stateful Nested Navigationの例を参照してください。
カスタムStatefulShellRouteの例では、ブランチナビゲータ用のコンテナをカスタマイズする方法と、ブランチ切り替え時のアニメーション遷移を実装する方法を示します。
継承
オブジェクト RouteBase StatefulShellRoute

で違いはなんなのか?

今回はボトムナビゲーションバーのコードで比較してみようと思います。
以前のコードだと、こんな複雑でしたが、新しい書き方は見やすくなりました。以前はこの通りに設定して使っていましたが、このコードは好きになれませんでした🙅

✅以前の書き方

ルートの設定も同じファイルに書いてあって、コードの記述量が多くて、とりあえず公式の通りに書いて使ってました。動けばいいという感覚で使ってましたので、ボタンをタップしたら、このパスのルートに画面遷移するのだろうなと、理解して使ってました。

import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';

/// を持つScaffoldを構築することによって、アプリの「シェル」を構築します。
/// BottomNavigationBarを構築し、[child]はScaffoldの本体に配置されます。
class ScaffoldWithNavBar extends StatelessWidget {
  /// Constructs an [ScaffoldWithNavBar].
  const ScaffoldWithNavBar({
    required this.child,
    Key? key,
  }) : super(key: key);

  /// Scaffoldのボディに表示するウィジェットです。
  /// このサンプルではNavigatorです。
  final Widget child;

  
  Widget build(BuildContext context) {

    return Scaffold(
      body: child,
      bottomNavigationBar: BottomNavigationBar(
        type: BottomNavigationBarType.fixed,
        items: const <BottomNavigationBarItem>[
          BottomNavigationBarItem(
            icon: Icon(Icons.home),
            label: 'A Screen',
          ),
          BottomNavigationBarItem(
            icon: Icon(Icons.business),
            label: 'B Screen',
          ),
          BottomNavigationBarItem(
            icon: Icon(Icons.notification_important_rounded),
            label: 'C Screen',
          ),
          BottomNavigationBarItem(
            icon: Icon(Icons.settings),
            label: 'D Screen',
          ),
        ],
        currentIndex: _calculateSelectedIndex(context),
        onTap: (int idx) => _onItemTapped(idx, context),
      ),
    );
  }

  static int _calculateSelectedIndex(BuildContext context) {
    final String location = GoRouterState.of(context).location;
    if (location.startsWith('/a')) {
      return 0;
    }
    if (location.startsWith('/b')) {
      return 1;
    }
    if (location.startsWith('/c')) {
      return 2;
    }
    // 追加したコード。0から数えて追加するみたいです.
    if (location.startsWith('/d')) {
      return 3;
    }
    return 0;
  }

  void _onItemTapped(int index, BuildContext context) {
    switch (index) {
      case 0:
        GoRouter.of(context).go('/a');
        break;
      case 1:
        GoRouter.of(context).go('/b');
        break;
      case 2:
        GoRouter.of(context).go('/c');
        break;
      // 追加したコード
      case 3:
        GoRouter.of(context).go('/d');
        break;
    }
  }
}

✅新しい書き方

このコードを書くだけで、ボトムナビゲーションバーをgo_routerで使えるようになる。新しい書き方は、短いコードで、ボトムナビゲーションバー作ってくれるウィジェットがあるのだなとパッと見ただけで分かりました。最初からこんな風に作って欲しかったですね。

import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';

class ScaffoldWithNavbar extends StatelessWidget {
  const ScaffoldWithNavbar(this.navigationShell, {super.key});

  /// ブランチ・ナビゲーターのナビゲーション・シェルとコンテナ。
  final StatefulNavigationShell navigationShell;

  
  Widget build(BuildContext context) {
    return Scaffold(
      body: navigationShell,// body: navigationShellは、IndexedStackをラップしています。
      bottomNavigationBar: BottomNavigationBar(// bottomNavigationBar: BottomNavigationBarは、ボトムナビゲーションバーを実装しています。
        currentIndex: navigationShell.currentIndex,// currentIndex: navigationShell.currentIndexは、現在のインデックスを取得しています。
        items: const [
          // BottomNavigationBarItemは、ボトムナビゲーションバーのアイテムを実装しています。
          BottomNavigationBarItem(icon: Icon(Icons.feed), label: 'Feed'),
          BottomNavigationBarItem(icon: Icon(Icons.shop), label: 'Shope'),
          BottomNavigationBarItem(icon: Icon(Icons.person), label: 'Profile'),
        ],
        onTap: _onTap,
      ),
    );
  }
  // _onTapメソッドは、ボトムナビゲーションバーのアイテムをタップしたときに、
  // そのアイテムのインデックスを取得して、そのインデックスに対応するブランチにナビゲートします。
  void _onTap(index) {
    navigationShell.goBranch(
      index,
      // ボトムナビゲーションバーを使用する際の一般的なパターンは、次のようなものです。
      // 既にアクティブになっているアイテムをタップしたときに、最初の場所に移動することをサポートすることです。
      // この例では、この動作をサポートする方法を示します。この例では、この動作をサポートする方法を示します、
      // goBranchのinitialLocationパラメータを使用します。
      initialLocation: index == navigationShell.currentIndex,
    );
  }
}

✅ルートの設定はこんな感じでやります。

画面遷移をするときに、状態を維持することができるらしい。ボトムナビゲーションバーのページの切り替えのルーティングは、以前のコードではなく、こちらのファイルに書くみたいです。

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import 'package:go_router_stateful/common/router_path.dart';
import 'package:go_router_stateful/modules/details/details_page.dart';
import 'package:go_router_stateful/modules/feed/feed_page.dart';

import '../modules/scaffold_with_navbar/scaffold_with_navbar.dart';

// このGlobalKeyは、GoRouterのインスタンスを取得するために必要です。
final _rootNavigatorKey = GlobalKey<NavigatorState>();
// このGlobalKeyは、各タブのGoRouterインスタンスを取得するために必要です。
final _sectionNavigatorKey = GlobalKey<NavigatorState>();
// このProviderは、GoRouterのインスタンスを取得するために必要です。
final goRouterProvider = Provider<GoRouter>((ref) {
  return GoRouter(
    navigatorKey: _rootNavigatorKey,// navigatorKeyとは、GoRouterのインスタンスを取得するために必要です。
    initialLocation: RouterPath.feed,// initialLocationは、アプリの最初のルートを指定します。
    routes: <RouteBase>[
      // StatefulShellRouteとは、GoRouterのルートを状態管理するためのものです。
      // indexedStackとは、StatefulShellRouteの一種で、
      // この場合、StatefulShellRouteは、IndexedStackをラップしています。
      StatefulShellRoute.indexedStack(
        builder: (context, state, navigationShell) {
          // カスタムシェル(BottomNavigationBar など)を実装したウィジェットを返します。
          // ステートフルな方法で他のブランチにナビゲートできるように、 // [StatefulNavigationShell] が渡されます。
          return ScaffoldWithNavbar(navigationShell);
        },
        // branchesとは、IndexedStackの子ウィジェットとして表示されるGoRouterのブランチを指定します。
        branches: [
          // ボトムナビゲーションバーのルート分岐1
          StatefulShellBranch(
            navigatorKey: _sectionNavigatorKey,
            // このブランチルートを追加する
            // 各ルートとそのサブルート (利用可能な場合) 例: feed/uuid/details
            routes: <RouteBase>[
              GoRoute(
                path: RouterPath.feed,
                builder: (context, state) => const FeedPage(),
                routes: <RouteBase>[
                  GoRoute(
                    path: RouterPath.details,
                    builder: (context, state) {
                      return const DetailsPage(label: 'FeedDetailsをFullPathで渡す');
                    },
                  )
                ],
              ),
            ],
          ),
          // ボトムナビゲーションバーのルート分岐2
          StatefulShellBranch(routes: <RouteBase>[
            // このブランチルートを追加する
            // 各ルートとそのサブルート (利用可能な場合) 例: shope/uuid/details
            GoRoute(
              path: RouterPath.shope,
              builder: (context, state) {
                return const DetailsPage(label: 'Shope');
              },
            ),
          ]),
          // ボトムナビゲーションバーのルート分岐3を追加
          StatefulShellBranch(routes: <RouteBase>[
            // このブランチルートを追加する
            // 各ルートとそのサブルート (利用可能な場合) 例: profile/uuid/details
            GoRoute(
              path: RouterPath.profile,
              builder: (context, state) {
                return const DetailsPage(label: 'Profile');
              },
            ),
          ]),
        ],
      ),
    ],
  );
});

Demo

まとめ

新しく追加された機能で、ボトムナビゲーションバーを使ってみましたが以前のように、ifとswitchを書かないので、コードが短くなりスッキリしましたし、タップしたら、StatefulShellBranchを使用して、指定されたルートへ画面遷移してくれる便利機能なのだということが分かりました。

今回話題には出せませんでしたが、以前から問題になっていた画面遷移の問題が解決されているのでしょうけど、使ってみた感じまだ実感がなかったので、使いながら検証してみようと思います。

Jboy王国メディア

Discussion