TOP(About this memo)) > 一覧(Flutter) > go_router
GoRouter
  initialLocation: String? initialLocation
  redirect: FutureOr<String?> Function(BuildContext, GoRouterState)?
  refreshListenable: Listenable? 
  navigatorKey: GlobalKey<NavigatorState>?
  onException: void Function(BuildContext, GoRouterState, GoRouter)?
  debugLogDiagnostics: bool
  routes: [
    GoRoute
      path: String
      name: String
      builder: 
          Widget Function(
            BuildContext context,
            GoRouterState state,
        )
      routes: <RouteBase>[] 
    ...
    ShellRoute
      builder: Widget Function(
            BuildContext context,
            GoRouterState state,
            Widget child,
        ),
      branches: <RouteBase>[
        GoRoute
      ]
  ]
GoRouter
GoRoute
ShellRoute/StatefulShellRoute
| 機能 | メソッド | 動作 | 
|---|---|---|
| go | GoRouter.go(…) または BuildContext.go(…) | 親ルートからサブルートへ移動したときのみスタックに積まれる。 実行前のスタックは全てリセットされる。 | 
| push | GoRouter.push(…) または BuildContext.go(…) | どのページへの移動でもスタックへ積まれる | 
| push | GoRouter.pop(…) または BuildContext.pop(…) | スタックの先頭要素をpopする | 
BuildContextから呼ぶことができるのは、以下のようにextensionによって拡張が行われているため。
extension GoRouterHelper on BuildContext {
    //...
    void go(String location, {Object? extra}) =>
        GoRouter.of(this).go(location, extra: extra);
    //...
GoRouter.push, popについては、NavigatorState.push, popと等価?
GoRouter.pushNamed, goNamed
画面の構成と対応するオブジェクトの構成のイメージは下記のようになる。
Drawer
ボトムナビゲーション
サンプルコード
final GlobalKey<NavigatorState> rootNavigatorKey = GlobalKey<NavigatorState>();
final GlobalKey<ScaffoldState> rootScaffoldKey = GlobalKey<ScaffoldState>();
void main() => runApp(MaterialApp.router(routerConfig: router));
final router = GoRouter(
navigatorKey: rootNavigatorKey,
initialLocation: "/a",
routes: <RouteBase>[
    StatefulShellRoute.indexedStack(
    builder: (BuildContext context, GoRouterState state,
        StatefulNavigationShell child) {
        return Scaffold(
        key: rootScaffoldKey,
        drawer: const Drawer(
            child: Align(
            alignment: Alignment.center,
            child: Text("drawer"),
            ),
        ),
        body: child,
        bottomNavigationBar: BottomNavigationBar(
            items: const <BottomNavigationBarItem>[
            BottomNavigationBarItem(
                icon: Icon(Icons.home),
                label: "A",
            ),
            BottomNavigationBarItem(
                icon: Icon(Icons.business),
                label: "B",
            ),
            BottomNavigationBarItem(
                icon: Icon(Icons.notification_important_rounded),
                label: "C",
            ),
            ],
            currentIndex: child.currentIndex,
            onTap: (int idx) {
            child.goBranch(
                idx,
                // 既にアクティブな箇所をタップした際は、initialLocationへ遷移させる。
                // https://github.com/flutter/packages/blob/main/packages/go_router/example/lib/stateful_shell_route.dart#L176
                initialLocation: idx == child.currentIndex,
            );
            },
        ),
        );
    },
    branches: <StatefulShellBranch>[
        StatefulShellBranch(routes: [
        GoRoute(
            path: "/a",
            builder: (context, _) => getDemoScaf(
            "/a",
            ),
        ),
        ]),
        StatefulShellBranch(routes: [
        GoRoute(
            path: "/b",
            builder: (context, _) => getDemoScaf("/b",
                onTextButtonPress: () => GoRouter.of(context).go("/b/bc"),
                buttonText: "go /b/bc"),
            routes: [
                GoRoute(
                path: "bc",
                builder: (context, _) =>
                    getDemoScaf("/b/bc", openDrawer: false),
                ),
            ]),
        ]),
        StatefulShellBranch(routes: [
        GoRoute(
            path: "/c",
            builder: (context, _) => getDemoScaf(
            "/c",
            ),
        )
        ])
    ],
    )
],
);
Scaffold getDemoScaf(String title,
        {void Function()? onTextButtonPress,
        String? buttonText,
        bool openDrawer = true}) =>
    Scaffold(
    drawer: const Drawer(
        child: Align(
        alignment: Alignment.center,
        child: Text("drawer"),
        ),
    ),
    appBar: AppBar(
        title: Text(title),
        leading: openDrawer
            ? IconButton(
                icon: const Icon(Icons.menu),
                onPressed: () => rootScaffoldKey.currentState!.openDrawer(),
            )
            : null,
    ),
    body: onTextButtonPress == null
        ? null
        : TextButton(
            onPressed: onTextButtonPress,
            child: Text(buttonText ?? "button"),
            ),
    );
任意の画面
    プロフィール画面(フルスクリーン)(「戻る」が可能)
        プロフィール編集画面(「戻る」が可能)
final GlobalKey<NavigatorState> rootNavigatorKey = GlobalKey<NavigatorState>();
void main() => runApp(MaterialApp.router(routerConfig: router));
final router = GoRouter(
    navigatorKey: rootNavigatorKey,
    initialLocation: "/a",
    routes: <RouteBase>[
        GoRoute(
        // 12以前の場合は必要
        // parentNavigatorKey: rootNavigatorKey,
        path: "/profile",
        pageBuilder: (context, _) {
            return MaterialPage(
            fullscreenDialog: true,
            child: getDemoScaf(
                "/profile",
                onTextButtonPress: () => GoRouter.of(context).push("/profile/edit"),
                buttonText: "push /profile/edit",
            ),
            );
        },
        routes: <RouteBase>[
            GoRoute(
            // 12以前の場合は必要
            // parentNavigatorKey: rootNavigatorKey,
            path: "edit",
            builder: (BuildContext context, GoRouterState state) {
                return getDemoScaf(
                "/profile/edit",
                );
            },
            ),
        ],
        ),
        ShellRoute(
        builder: (BuildContext context, GoRouterState state, Widget child) {
            // ScaffoldWithNavBarは下記を参照
            // https://github.com/flutter/packages/blob/main/packages/go_router/example/lib/shell_route.dart#L118
            return ScaffoldWithNavBar(child: child);
        },
        routes: <RouteBase>[
            GoRoute(
            path: "/a",
            builder: (context, _) => getDemoScaf("/a",
                onTextButtonPress: () => GoRouter.of(context).push("/profile"),
                buttonText: "push /profile"),
            ),
            GoRoute(
                path: "/b",
                builder: (context, _) => getDemoScaf("/b",
                    onTextButtonPress: () => GoRouter.of(context).go("/b/bc"),
                    buttonText: "go /b/bc"),
                routes: [
                GoRoute(
                    path: "bc",
                    builder: (context, _) => getDemoScaf("/b/bc",
                        onTextButtonPress: () =>
                            GoRouter.of(context).push("/profile"),
                        buttonText: "push /profile"),
                ),
                ]),
            GoRoute(
            path: "/c",
            builder: (context, _) => getDemoScaf("/c",
                onTextButtonPress: () => GoRouter.of(context).push("/profile"),
                buttonText: "push /profile"),
            )
        ],
        )
    ],
);
Scaffold getDemoScaf(
    String title, {
    void Function()? onTextButtonPress,
    String? buttonText,
}) =>
    Scaffold(
    appBar: AppBar(
        title: Text(title),
    ),
    body: onTextButtonPress == null
        ? null
        : TextButton(
            onPressed: onTextButtonPress,
            child: Text(buttonText ?? "button"),
            ),
    );
| 機能 | 原因 | ハンドリングの例 | 
|---|---|---|
| GoError/AssertionError | GoRouterが正しく使用されていない(ソースコードの修正が必要) | main関数でキャッチしてスタックトレース | 
| GoException | ルートが存在しない場合 | GoRouter.onExceptionでハンドリングしてエラー画面を表示 | 
void main() {
  testWidgets('', (WidgetTester tester) async {
    // Build our app and trigger a frame.
    await tester.pumpWidget(MaterialApp.router(
      routerConfig: GoRouter(
        routes: [
          GoRoute(
            path: '/',
            builder: (context, state) => const Text("home"),
          ),
          GoRoute(
            path: '/404',
            builder: (context, state) => const Text("not found"),
          )
        ],
        onException:
            (BuildContext context, GoRouterState state, GoRouter router) {
          router.go('/404', extra: state.uri.toString());
        },
      ),
    ));
    await tester.pump();
    expect(find.text("home"), findsOne);
    tester.element(find.text("home")).go("/not/exist/route");
    await tester.pumpAndSettle();
    expect(find.text("not found"), findsOne);
    expect(find.text("home"), findsNothing);
  });
}
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:go_router/go_router.dart';
final GlobalKey<NavigatorState> rootNavigatorKey = GlobalKey<NavigatorState>();
// flutter test --plain-name 'GoRouter'
void main() {
getGoRouter(String path, [List<RouteBase> children = const []]) => GoRoute(
    path: path,
    routes: children,
    builder: (_, __) {
        debugPrint(path);
        return Scaffold(
            appBar: AppBar(
                title: Text(path),
            ),
        );
    });
testWidgets("GoRouter", (tester) async {
    await tester.pumpWidget(MaterialApp.router(
    routerConfig: GoRouter(
        navigatorKey: rootNavigatorKey,
        debugLogDiagnostics: true,
        routes: [
            getGoRouter("/"),
            getGoRouter("/a", [getGoRouter("b")]),
            getGoRouter("/c", [
                getGoRouter("d", [getGoRouter("e")])
            ]),
        ],
        redirect: (BuildContext context, GoRouterState state) {
            debugPrint("redirect");
            return null;
        },
    ),
    ));
    debugPrint("");
    for (final t in [
    (
        "go",
        "/a/b",
        (GoRouter r, String path) => r.go(path),
    ),
    (
        "pop",
        "",
        (GoRouter r, String path) => r.pop(),
    ),
    (
        "push",
        "/c/d/e",
        (GoRouter r, String path) => r.push(path),
    ),
    (
        "pop",
        "",
        (GoRouter r, String path) => r.pop(),
    ),
    (
        "go",
        "/a",
        (GoRouter r, String path) => r.go(path),
    ),
    ]) {
    debugPrint("${t.$1}${t.$2 != "" ? " ${t.$2}" : ""}: ");
    t.$3(GoRouter.of(rootNavigatorKey.currentContext!), t.$2);
    await tester.pumpAndSettle();
    debugPrint(Navigator.of(rootNavigatorKey.currentContext!)
        .widget
        .pages
        .toString());
    debugPrint("");
    }
});
}
/*
redirect
/
go /a/b: 
redirect
/a
b
[MaterialPage<void>("/a", [<'/a'>], {}), MaterialPage<void>("b", [<'/a/b'>], {})]
pop: 
/a
[MaterialPage<void>("/a", [<'/a'>], {})]
push /c/d/e: 
redirect
e
/a
[MaterialPage<void>("/a", [<'/a'>], {}), MaterialPage<void>("e", [<'gnyrwj[qh_jmsptntylphgqjhd]teli\'>], {})]
pop: 
/a
[MaterialPage<void>("/a", [<'/a'>], {})]
go /a: 
redirect
/a
[MaterialPage<void>("/a", [<'/a'>], {})]
*/
GoRouter(
    redirect: (context, state) {
        final path = state.uri.path;
        //...
    }
)
FamilyRoute(f.id).go(context)
$appRoutesを設定する。final _router = GoRouter(routes: $appRoutes, /* ...  */);
class FamilyRoute extends GoRouteData {
const FamilyRoute(this.fid);
final String fid;
@override
Widget build(BuildContext context, GoRouterState state) =>
    FamilyScreen(family: familyById(fid));
}
@TypedGoRoute<HomeRoute>(
path: '/',
routes: <TypedGoRoute<GoRouteData>>[
    TypedGoRoute<FamilyRoute>(
    path: 'family/:fid',
    routes: <TypedGoRoute<GoRouteData>>[
        TypedGoRoute<PersonRoute>(
        path: 'person/:pid',
        routes: <TypedGoRoute<GoRouteData>>[
            TypedGoRoute<PersonDetailsRoute>(path: 'details/:details'),
        ],
        ),
    ],
    ),
    TypedGoRoute<FamilyCountRoute>(path: 'family-count/:count'),
],
)
flutter pub run build_runner buildによって上記からGoRouteの定義が生成される。The following StateError was thrown while dispatching notifications for GoRouteInformationProvider:
Bad state: Origin is only applicable schemes http and https: myapp://details
When the exception was thrown, this was the stack:
#0      _Uri.origin (dart:core/uri.dart:2829:7)
#1      GoRouteInformationParser.parseRouteInformationWithDependencies (package:go_router/src/parser.dart:83:47)
...
String get origin {
    // ...
    if (scheme != "http" && scheme != "https") {
        throw StateError(
        "Origin is only applicable schemes http and https: $this");
    }
    //...
     return "$scheme://$host:$port";
}
void main() {
    final uri = Uri.parse("http://test.com/aaaa");
    print("scheme: ${uri.scheme}, origin: ${uri.origin}, host: ${uri.host}");
    
    final uri2 = Uri.parse("myApp://hoge/details");
    print("scheme: ${uri2.scheme}, host: ${uri2.host}");// 
    print("origin: ${uri2.origin}"); // error
    // scheme: http, origin: http://test.com, host: test.com
    // scheme: myapp, host: hoge
    // エラー
}
GoRouter.of(context).pop();// このpopが実行されない。
GoRouter.of(context).refresh();
GoRouter.of(context).pop();
await tester.pumpAndSettle(); //別のフレームで実行するとpopが動作する
GoRouter.of(context).refresh();
GoRouter.of(context).pop();
await Future.delayed(const Duration(milliseconds: 601));
GoRouter.of(context).refresh();
void main() {
  testWidgets("GoRouter", (tester) async {
    await tester.pumpWidget(MaterialApp.router(
      routerConfig: GoRouter(
        navigatorKey: rootNavigatorKey,
        redirect: (context, state) {
          GoRouter.of(context);
          return null;
        },
        routes: [],
      ),
    ));
    await tester.pump();
  });
}
/*
...
No GoRouter found in context
...
When the exception was thrown, this was the stack:
#2      GoRouter.of (package:go_router/src/router.dart:507:12)
#3      main.<anonymous closure>.<anonymous closure> (file:///〜/flutter_application_12/test/widget_test.dart:14:20)
#4      RouteConfiguration.redirect.processRedirect (package:go_router/src/configuration.dart:417:80)
#5      RouteConfiguration.redirect (package:go_router/src/configuration.dart:429:14)
#6      GoRouteInformationParser._redirect (package:go_router/src/parser.dart:169:10)
#7      GoRouteInformationParser.parseRouteInformationWithDependencies (package:go_router/src/parser.dart:104:32)
#8      _RouterState._processRouteInformation (package:flutter/src/widgets/router.dart:741:8) 
...
*/