[go_router]: implemented helpers for ShellRoute (#2730)
* [Feature]: implemented helpers for ShellRoute
* [bugfix]: change export
* Removed named private constructor for TypedRoute
* Update CHANGELOG.md
* Update pubspec.yaml
* [Feature]: split routes per type
* [Feature]: add new exports
* Tests, refactor routes
* remove line in changelog
* Add navigatorKey for each route
* Fixed tests
* Format
* Bump version in pubspec.yaml
* Update packages/go_router/lib/src/route_data.dart
Co-authored-by: chunhtai <47866232+chunhtai@users.noreply.github.com>
* Update packages/go_router/lib/src/route_data.dart
Co-authored-by: chunhtai <47866232+chunhtai@users.noreply.github.com>
* Removed buildPageWithState tests
* Bump version in pubspec
* Address comments from code review
* Export ShellRouteData
* Remove unnecessary import
---------
Co-authored-by: chunhtai <47866232+chunhtai@users.noreply.github.com>
Co-authored-by: John Ryan <ryjohn@google.com>
diff --git a/packages/go_router/CHANGELOG.md b/packages/go_router/CHANGELOG.md
index 5b9c7d4..e7346b2 100644
--- a/packages/go_router/CHANGELOG.md
+++ b/packages/go_router/CHANGELOG.md
@@ -1,3 +1,7 @@
+## 6.0.10
+
+- Adds helpers for go_router_builder for ShellRoute support
+
## 6.0.9
- Fixes deprecation message for `GoRouterState.namedLocation`
diff --git a/packages/go_router/lib/go_router.dart b/packages/go_router/lib/go_router.dart
index ad2a74f..b60ae60 100644
--- a/packages/go_router/lib/go_router.dart
+++ b/packages/go_router/lib/go_router.dart
@@ -11,7 +11,8 @@
export 'src/misc/extensions.dart';
export 'src/misc/inherited_router.dart';
export 'src/pages/custom_transition_page.dart';
-export 'src/route_data.dart' show GoRouteData, TypedGoRoute;
+export 'src/route_data.dart'
+ show GoRouteData, TypedGoRoute, TypedShellRoute, ShellRouteData;
export 'src/router.dart';
export 'src/typedefs.dart'
show GoRouterPageBuilder, GoRouterRedirect, GoRouterWidgetBuilder;
diff --git a/packages/go_router/lib/src/route_data.dart b/packages/go_router/lib/src/route_data.dart
index d577340..3530ca2 100644
--- a/packages/go_router/lib/src/route_data.dart
+++ b/packages/go_router/lib/src/route_data.dart
@@ -11,13 +11,19 @@
import 'route.dart';
import 'state.dart';
+/// A superclass for each route data
+abstract class RouteData {
+ /// Default const constructor
+ const RouteData();
+}
+
/// Baseclass for supporting
/// [Type-safe routing](https://pub.dev/documentation/go_router/latest/topics/Type-safe%20routes-topic.html).
///
/// Subclasses must override one of [build], [buildPage], or
/// [redirect].
/// {@category Type-safe routes}
-abstract class GoRouteData {
+abstract class GoRouteData extends RouteData {
/// Allows subclasses to have `const` constructors.
///
/// [GoRouteData] is abstract and cannot be instantiated directly.
@@ -74,7 +80,8 @@
static GoRoute $route<T extends GoRouteData>({
required String path,
required T Function(GoRouterState) factory,
- List<GoRoute> routes = const <GoRoute>[],
+ GlobalKey<NavigatorState>? parentNavigatorKey,
+ List<RouteBase> routes = const <RouteBase>[],
}) {
T factoryImpl(GoRouterState state) {
final Object? extra = state.extra;
@@ -103,6 +110,7 @@
pageBuilder: pageBuilder,
redirect: redirect,
routes: routes,
+ parentNavigatorKey: parentNavigatorKey,
);
}
@@ -111,26 +119,138 @@
static final Expando<GoRouteData> _stateObjectExpando = Expando<GoRouteData>(
'GoRouteState to GoRouteData expando',
);
+
+ /// [navigatorKey] is used to point to a certain navigator
+ ///
+ /// It will use the given key to find the right navigator for [GoRoute]
+ GlobalKey<NavigatorState>? get navigatorKey => null;
}
-/// Annotation for types that support typed routing.
+/// Base class for supporting
+/// [nested navigation](https://pub.dev/packages/go_router#nested-navigation)
+abstract class ShellRouteData extends RouteData {
+ /// Default const constructor
+ const ShellRouteData();
+
+ /// [pageBuilder] is used to build the page
+ Page<void> pageBuilder(
+ BuildContext context,
+ GoRouterState state,
+ Widget navigator,
+ ) =>
+ const NoOpPage();
+
+ /// [pageBuilder] is used to build the page
+ Widget builder(
+ BuildContext context,
+ GoRouterState state,
+ Widget navigator,
+ ) =>
+ throw UnimplementedError(
+ 'One of `builder` or `pageBuilder` must be implemented.',
+ );
+
+ /// A helper function used by generated code.
+ ///
+ /// Should not be used directly.
+ static ShellRoute $route<T extends ShellRouteData>({
+ required T Function(GoRouterState) factory,
+ GlobalKey<NavigatorState>? navigatorKey,
+ List<RouteBase> routes = const <RouteBase>[],
+ }) {
+ T factoryImpl(GoRouterState state) {
+ final Object? extra = state.extra;
+
+ // If the "extra" value is of type `T` then we know it's the source
+ // instance of `GoRouteData`, so it doesn't need to be recreated.
+ if (extra is T) {
+ return extra;
+ }
+
+ return (_stateObjectExpando[state] ??= factory(state)) as T;
+ }
+
+ Widget builder(
+ BuildContext context,
+ GoRouterState state,
+ Widget navigator,
+ ) =>
+ factoryImpl(state).builder(
+ context,
+ state,
+ navigator,
+ );
+
+ Page<void> pageBuilder(
+ BuildContext context,
+ GoRouterState state,
+ Widget navigator,
+ ) =>
+ factoryImpl(state).pageBuilder(
+ context,
+ state,
+ navigator,
+ );
+
+ return ShellRoute(
+ builder: builder,
+ pageBuilder: pageBuilder,
+ routes: routes,
+ navigatorKey: navigatorKey,
+ );
+ }
+
+ /// Used to cache [ShellRouteData] that corresponds to a given [GoRouterState]
+ /// to minimize the number of times it has to be deserialized.
+ static final Expando<ShellRouteData> _stateObjectExpando =
+ Expando<ShellRouteData>(
+ 'GoRouteState to ShellRouteData expando',
+ );
+
+ /// It will be used to instantiate [Navigator] with the given key
+ GlobalKey<NavigatorState>? get navigatorKey => null;
+}
+
+/// A superclass for each typed route descendant
+class TypedRoute<T extends RouteData> {
+ /// Default const constructor
+ const TypedRoute();
+}
+
+/// A superclass for each typed go route descendant
@Target(<TargetKind>{TargetKind.library, TargetKind.classType})
-class TypedGoRoute<T extends GoRouteData> {
- /// Instantiates a new instance of [TypedGoRoute].
+class TypedGoRoute<T extends GoRouteData> extends TypedRoute<T> {
+ /// Default const constructor
const TypedGoRoute({
required this.path,
- this.routes = const <TypedGoRoute<GoRouteData>>[],
+ this.routes = const <TypedRoute<RouteData>>[],
});
- /// The path that corresponds to this rout.
+ /// The path that corresponds to this route.
///
/// See [GoRoute.path].
+ ///
+ ///
final String path;
/// Child route definitions.
///
- /// See [GoRoute.routes].
- final List<TypedGoRoute<GoRouteData>> routes;
+ /// See [RouteBase.routes].
+ final List<TypedRoute<RouteData>> routes;
+}
+
+/// A superclass for each typed shell route descendant
+@Target(<TargetKind>{TargetKind.library, TargetKind.classType})
+class TypedShellRoute<T extends ShellRouteData> extends TypedRoute<T> {
+ /// Default const constructor
+ const TypedShellRoute({
+ this.routes = const <TypedRoute<RouteData>>[],
+ });
+
+ /// Child route definitions.
+ ///
+ /// See [RouteBase.routes].
+ final List<TypedRoute<RouteData>> routes;
}
/// Internal class used to signal that the default page behavior should be used.
diff --git a/packages/go_router/pubspec.yaml b/packages/go_router/pubspec.yaml
index eddc355..0fbec80 100644
--- a/packages/go_router/pubspec.yaml
+++ b/packages/go_router/pubspec.yaml
@@ -1,7 +1,7 @@
name: go_router
description: A declarative router for Flutter based on Navigation 2 supporting
deep linking, data-driven routes and more
-version: 6.0.9
+version: 6.0.10
repository: https://github.com/flutter/packages/tree/main/packages/go_router
issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+go_router%22
diff --git a/packages/go_router/test/route_data_test.dart b/packages/go_router/test/route_data_test.dart
index 6c8a49a..1541940 100644
--- a/packages/go_router/test/route_data_test.dart
+++ b/packages/go_router/test/route_data_test.dart
@@ -15,11 +15,36 @@
const SizedBox(key: Key('build'));
}
+class _ShellRouteDataBuilder extends ShellRouteData {
+ const _ShellRouteDataBuilder();
+
+ @override
+ Widget builder(
+ BuildContext context,
+ GoRouterState state,
+ Widget navigator,
+ ) =>
+ SizedBox(
+ key: const Key('builder'),
+ child: navigator,
+ );
+}
+
final GoRoute _goRouteDataBuild = GoRouteData.$route(
path: '/build',
factory: (GoRouterState state) => const _GoRouteDataBuild(),
);
+final ShellRoute _shellRouteDataBuilder = ShellRouteData.$route(
+ factory: (GoRouterState state) => const _ShellRouteDataBuilder(),
+ routes: <RouteBase>[
+ GoRouteData.$route(
+ path: '/child',
+ factory: (GoRouterState state) => const _GoRouteDataBuild(),
+ ),
+ ],
+);
+
class _GoRouteDataBuildPage extends GoRouteData {
const _GoRouteDataBuildPage();
@override
@@ -29,11 +54,38 @@
);
}
+class _ShellRouteDataPageBuilder extends ShellRouteData {
+ const _ShellRouteDataPageBuilder();
+
+ @override
+ Page<void> pageBuilder(
+ BuildContext context,
+ GoRouterState state,
+ Widget navigator,
+ ) =>
+ MaterialPage<void>(
+ child: SizedBox(
+ key: const Key('page-builder'),
+ child: navigator,
+ ),
+ );
+}
+
final GoRoute _goRouteDataBuildPage = GoRouteData.$route(
path: '/build-page',
factory: (GoRouterState state) => const _GoRouteDataBuildPage(),
);
+final ShellRoute _shellRouteDataPageBuilder = ShellRouteData.$route(
+ factory: (GoRouterState state) => const _ShellRouteDataPageBuilder(),
+ routes: <RouteBase>[
+ GoRouteData.$route(
+ path: '/child',
+ factory: (GoRouterState state) => const _GoRouteDataBuild(),
+ ),
+ ],
+);
+
class _GoRouteDataRedirectPage extends GoRouteData {
const _GoRouteDataRedirectPage();
@override
@@ -53,56 +105,82 @@
];
void main() {
- testWidgets(
- 'It should build the page from the overridden build method',
- (WidgetTester tester) async {
- final GoRouter goRouter = GoRouter(
- initialLocation: '/build',
- routes: _routes,
- );
- await tester.pumpWidget(MaterialApp.router(
- routeInformationProvider: goRouter.routeInformationProvider,
- routeInformationParser: goRouter.routeInformationParser,
- routerDelegate: goRouter.routerDelegate,
- ));
- expect(find.byKey(const Key('build')), findsOneWidget);
- expect(find.byKey(const Key('buildPage')), findsNothing);
- },
- );
+ group('GoRouteData', () {
+ testWidgets(
+ 'It should build the page from the overridden build method',
+ (WidgetTester tester) async {
+ final GoRouter goRouter = GoRouter(
+ initialLocation: '/build',
+ routes: _routes,
+ );
+ await tester.pumpWidget(MaterialApp.router(
+ routeInformationProvider: goRouter.routeInformationProvider,
+ routeInformationParser: goRouter.routeInformationParser,
+ routerDelegate: goRouter.routerDelegate,
+ ));
+ expect(find.byKey(const Key('build')), findsOneWidget);
+ expect(find.byKey(const Key('buildPage')), findsNothing);
+ },
+ );
- testWidgets(
- 'It should build the page from the overridden buildPage method',
- (WidgetTester tester) async {
- final GoRouter goRouter = GoRouter(
- initialLocation: '/build-page',
- routes: _routes,
- );
- await tester.pumpWidget(MaterialApp.router(
- routeInformationProvider: goRouter.routeInformationProvider,
- routeInformationParser: goRouter.routeInformationParser,
- routerDelegate: goRouter.routerDelegate,
- ));
- expect(find.byKey(const Key('build')), findsNothing);
- expect(find.byKey(const Key('buildPage')), findsOneWidget);
- },
- );
+ testWidgets(
+ 'It should build the page from the overridden buildPage method',
+ (WidgetTester tester) async {
+ final GoRouter goRouter = GoRouter(
+ initialLocation: '/build-page',
+ routes: _routes,
+ );
+ await tester.pumpWidget(MaterialApp.router(
+ routeInformationProvider: goRouter.routeInformationProvider,
+ routeInformationParser: goRouter.routeInformationParser,
+ routerDelegate: goRouter.routerDelegate,
+ ));
+ expect(find.byKey(const Key('build')), findsNothing);
+ expect(find.byKey(const Key('buildPage')), findsOneWidget);
+ },
+ );
+ });
- testWidgets(
- 'It should build the page from the overridden buildPage method',
- (WidgetTester tester) async {
- final GoRouter goRouter = GoRouter(
- initialLocation: '/build-page-with-state',
- routes: _routes,
- );
- await tester.pumpWidget(MaterialApp.router(
- routeInformationProvider: goRouter.routeInformationProvider,
- routeInformationParser: goRouter.routeInformationParser,
- routerDelegate: goRouter.routerDelegate,
- ));
- expect(find.byKey(const Key('build')), findsNothing);
- expect(find.byKey(const Key('buildPage')), findsNothing);
- },
- );
+ group('ShellRouteData', () {
+ testWidgets(
+ 'It should build the page from the overridden build method',
+ (WidgetTester tester) async {
+ final GoRouter goRouter = GoRouter(
+ initialLocation: '/child',
+ routes: <RouteBase>[
+ _shellRouteDataBuilder,
+ ],
+ );
+ await tester.pumpWidget(MaterialApp.router(
+ routeInformationProvider: goRouter.routeInformationProvider,
+ routeInformationParser: goRouter.routeInformationParser,
+ routerDelegate: goRouter.routerDelegate,
+ ));
+ expect(find.byKey(const Key('builder')), findsOneWidget);
+ expect(find.byKey(const Key('page-builder')), findsNothing);
+ },
+ );
+
+ testWidgets(
+ 'It should build the page from the overridden buildPage method',
+ (WidgetTester tester) async {
+ final GoRouter goRouter = GoRouter(
+ initialLocation: '/child',
+ routes: <RouteBase>[
+ _shellRouteDataPageBuilder,
+ ],
+ );
+ await tester.pumpWidget(MaterialApp.router(
+ routeInformationProvider: goRouter.routeInformationProvider,
+ routeInformationParser: goRouter.routeInformationParser,
+ routerDelegate: goRouter.routerDelegate,
+ ));
+ expect(find.byKey(const Key('builder')), findsNothing);
+ expect(find.byKey(const Key('page-builder')), findsOneWidget);
+ },
+ );
+ });
+
testWidgets(
'It should redirect using the overridden redirect method',
(WidgetTester tester) async {