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)
...
*/