diff --git a/packages/go_router/CHANGELOG.md b/packages/go_router/CHANGELOG.md index 5b9c7d4bbd5c..e7346b2a7379 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 ad2a74f222c4..b60ae60115be 100644 --- a/packages/go_router/lib/go_router.dart +++ b/packages/go_router/lib/go_router.dart @@ -11,7 +11,8 @@ export 'src/configuration.dart' 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 d57734040364..3530ca250fc4 100644 --- a/packages/go_router/lib/src/route_data.dart +++ b/packages/go_router/lib/src/route_data.dart @@ -11,13 +11,19 @@ import 'package:meta/meta_meta.dart'; 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 @@ abstract class GoRouteData { static GoRoute $route({ required String path, required T Function(GoRouterState) factory, - List routes = const [], + GlobalKey? parentNavigatorKey, + List routes = const [], }) { T factoryImpl(GoRouterState state) { final Object? extra = state.extra; @@ -103,6 +110,7 @@ abstract class GoRouteData { pageBuilder: pageBuilder, redirect: redirect, routes: routes, + parentNavigatorKey: parentNavigatorKey, ); } @@ -111,26 +119,138 @@ abstract class GoRouteData { static final Expando _stateObjectExpando = Expando( '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? 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 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({ + required T Function(GoRouterState) factory, + GlobalKey? navigatorKey, + List routes = const [], + }) { + 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 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 _stateObjectExpando = + Expando( + 'GoRouteState to ShellRouteData expando', + ); + + /// It will be used to instantiate [Navigator] with the given key + GlobalKey? get navigatorKey => null; +} + +/// A superclass for each typed route descendant +class TypedRoute { + /// Default const constructor + const TypedRoute(); +} + +/// A superclass for each typed go route descendant @Target({TargetKind.library, TargetKind.classType}) -class TypedGoRoute { - /// Instantiates a new instance of [TypedGoRoute]. +class TypedGoRoute extends TypedRoute { + /// Default const constructor const TypedGoRoute({ required this.path, - this.routes = const >[], + this.routes = const >[], }); - /// 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> routes; + /// See [RouteBase.routes]. + final List> routes; +} + +/// A superclass for each typed shell route descendant +@Target({TargetKind.library, TargetKind.classType}) +class TypedShellRoute extends TypedRoute { + /// Default const constructor + const TypedShellRoute({ + this.routes = const >[], + }); + + /// Child route definitions. + /// + /// See [RouteBase.routes]. + final List> 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 eddc3552f281..0fbec8080dfc 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 6c8a49a6661b..15419406f645 100644 --- a/packages/go_router/test/route_data_test.dart +++ b/packages/go_router/test/route_data_test.dart @@ -15,11 +15,36 @@ class _GoRouteDataBuild extends GoRouteData { 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: [ + GoRouteData.$route( + path: '/child', + factory: (GoRouterState state) => const _GoRouteDataBuild(), + ), + ], +); + class _GoRouteDataBuildPage extends GoRouteData { const _GoRouteDataBuildPage(); @override @@ -29,11 +54,38 @@ class _GoRouteDataBuildPage extends GoRouteData { ); } +class _ShellRouteDataPageBuilder extends ShellRouteData { + const _ShellRouteDataPageBuilder(); + + @override + Page pageBuilder( + BuildContext context, + GoRouterState state, + Widget navigator, + ) => + MaterialPage( + 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: [ + GoRouteData.$route( + path: '/child', + factory: (GoRouterState state) => const _GoRouteDataBuild(), + ), + ], +); + class _GoRouteDataRedirectPage extends GoRouteData { const _GoRouteDataRedirectPage(); @override @@ -53,56 +105,82 @@ final List _routes = [ ]; 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); + }, + ); + }); + + group('ShellRouteData', () { + testWidgets( + 'It should build the page from the overridden build method', + (WidgetTester tester) async { + final GoRouter goRouter = GoRouter( + initialLocation: '/child', + routes: [ + _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: [ + _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 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); - }, - ); testWidgets( 'It should redirect using the overridden redirect method', (WidgetTester tester) async {