くらげになりたい。

くらげのようにふわふわ生きたい日曜プログラマなブログ。趣味の備忘録です。

Flutterのgo_routerでパスとタブを一致させる(StatefulShellRoute.indexedStack/BottomNavigationBar)

FlutterでBottomNavigationBarを使う場合、
currentIndexなどを使う必要があるけど、go_routerでパスと一致させたいなと思って、
いろいろ試したときの備忘録(*´ω`*)

StatefulShellRoute.indexedStackを使うといいらしい。

が、ドキュメントにはほぼ説明がないっぽい。。

サンプルコード

GoRouter部分はこんな感じ。

ShellRouteと同じような感じで、
固定したい部分(Scaffold,AppBar,BottomNavigationBarなど)と
遷移したい部分を分ける感じ。

import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_samples/layout/app_layout.dart';
import 'package:flutter_samples/pages/label_page.dart';
import 'package:go_router/go_router.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';

part "app_router.g.dart";

// rootのState付きNavigator
final GlobalKey<NavigatorState> _rootNavigatorKey =
    GlobalKey<NavigatorState>(debugLabel: 'root');

// "/tab/a" 配下のState付きNavigator
final GlobalKey<NavigatorState> _shellTabANavigatorKey =
    GlobalKey<NavigatorState>(debugLabel: 'shellTabA');
    
// "/tab/b" 配下のState付きNavigator
final GlobalKey<NavigatorState> _shellTabBNavigatorKey =
    GlobalKey<NavigatorState>(debugLabel: 'shellTabB');

@riverpod
class AppRouter extends _$AppRouter {
  @override
  GoRouter build() {
    return GoRouter(
      navigatorKey: _rootNavigatorKey,
      debugLogDiagnostics: kDebugMode,
      initialLocation: "/tab/a",
      routes: <RouteBase>[
        // index付きのStatefulShellRouteを使う
        StatefulShellRoute.indexedStack(
          // Scaffoldなど描画を固定したい部分のWidgetを返す
          builder: (context, state, navigationShell) {
            return AppLayout(navigationShell: navigationShell);
          },
          branches: [
            StatefulShellBranch(
              navigatorKey: _shellTabANavigatorKey,
              routes: [
                GoRoute(path: "/tab/a", builder: _labelPegeBuilder("TAB A")),
              ],
            ),
            StatefulShellBranch(
              navigatorKey: _shellTabBNavigatorKey,
              routes: [
                GoRoute(path: "/tab/b", builder: _labelPegeBuilder("TAB B"))
              ],
            ),
          ],
        ),
      ],
    );
  }

  // サンプルページを返すだけのBuilder
  GoRouterWidgetBuilder _labelPegeBuilder(String label) {
    return (context, state) => Center(child: Text(label));
  }
}

StatefulNavigationShell(navigationShell)が受け取れるようになり、
そこにpathと一致したGoRouteのWidget(navigationShell自体)と、
branches内のindex(navigationShell.currentIndex)を取得できる。

これらを使って、BottomNavigationBarに設定することで、
パスとタブを一致させることができるよう。

なので、ScaffoldBottomNavigationBarを含むAppLayoutはこんな感じ。

class AppLayout extends HookConsumerWidget {
  // navigationShellを受け取るように追加
  const AppLayout({super.key, required this.navigationShell});
  final StatefulNavigationShell navigationShell;

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    return Scaffold(
      appBar: AppBar(
        backgroundColor: Theme.of(context).colorScheme.inversePrimary,
        title: const Text("Flutter Samples"),
      ),
      // StatefulNavigationShellのWidgetを利用
      body: navigationShell,
      bottomNavigationBar: BottomNavigationBar(
        // StatefulNavigationShellのindexを利用
        currentIndex: navigationShell.currentIndex,
        // goBranch()を使って、タブを切り替え
        onTap: (index) => navigationShell.goBranch(index),
        items: const [
          BottomNavigationBarItem(
            icon: Icon(Icons.home_outlined),
            label: "TAB A",
          ),
          BottomNavigationBarItem(
            icon: Icon(Icons.article_outlined),
            label: "TAB B",
          )
        ],
      ),
    );
  }
}

ShellRoute

ドキュメントより翻訳&抜粋。

一致する子ルートの周囲にUI shellを表示するルート

ShellRouteがGoRouterかGoRouteのルートのリストに追加されると、 rootのNavigatorではなく、新しいNavigatorを使って、サブルートを表示する

別のNavigatorで子ルートを表示するには、GoRouterまたはShellRouteに提供されたキーと一致するparentNavigatorKey をそのナビゲーターに設定する。

  • ShellRouteでは、rootのNavigatorとは別に、
    新しいNavigatorを利用する
  • 同じNavigator上に表示したい場合は、
    parentNavigatorKeyを設定する

らしい

StatefulShellRoute

ドキュメントより翻訳&抜粋。

サブルート用に個別のNavigatorを備えた UI shellを表示するルート。

ShellRouteと似ているが、子ルートごとにNavigatorを持つ点で異なる。
BottomNavigationBarなどを使う場合に便利

ShellRouteと同様に、builderなどが必要だが、
StatefulNavigationShellを受け取る点で異なる。

StatefulShellRouteは一連のnavigation stacksを維持するため、
ブランチ間の切り替え時の遷移はbranch Navigator container(つまり、navigatorContainerBuilder) の責務である。

デフォルトのIndexedStackの実装であるStatefulShellRoute.indexedStackには、 アニメーションは含まれない。しかし、これを実現する例を提供する。

  • 子ルート(StatefulShellBranch)ごとに、新しいNavigatorを利用
  • StatefulShellBranch間のアニメーションは、自身で実装する必要がある。

サンプルはこのあたり。

NavigatorのStateクラス

ソースの中身はこんな感じ。_historyなどを持っている。

class NavigatorState extends State<Navigator> with TickerProviderStateMixin, RestorationMixin {
  late GlobalKey<OverlayState> _overlayKey;
  List<_RouteEntry> _history = <_RouteEntry>[];
// ...

  bool canPop() {
    final Iterator<_RouteEntry> iterator = _history.where(_RouteEntry.isPresentPredicate).iterator;
    if (!iterator.moveNext()) {
      // We have no active routes, so we can't pop.
      return false;
    }
}
class Navigator extends StatefulWidget {
  // Navigator.of(context)は、NavigatorStateを返す
  static NavigatorState of(BuildContext context, { bool rootNavigator = false }) {
  // ...
  }
  
  // canPopはNavigatorStateのcanPopを呼び出していて
  static bool canPop(BuildContext context) {
    final NavigatorState? navigator = Navigator.maybeOf(context);
    return navigator != null && navigator.canPop();
  }
}

// NavigatorState.canPopは_historyを見ている
class NavigatorState extends State<Navigator> with TickerProviderStateMixin, RestorationMixin {
  List<_RouteEntry> _history = <_RouteEntry>[];
// ...

  bool canPop() {
    final Iterator<_RouteEntry> iterator = _history.where(_RouteEntry.isPresentPredicate).iterator;
    if (!iterator.moveNext()) {
      // We have no active routes, so we can't pop.
      return false;
    }
    // ...
  }
}

なので、Navigatorが異なるというのは、スタック自体も別になっているよう。
parentNavigatorKeyを設定する=同じNavigatorのスタックを利用する
って感じなのかな(*´ω`*)?


以上!! GoRouterとかNavigatorとか、なんとなくわかってきた気がする...(*´ω`*)

参考にしたサイト様