diff --git a/packages/go_router/CHANGELOG.md b/packages/go_router/CHANGELOG.md index 6b20ae5cfcc..c345b25e88a 100644 --- a/packages/go_router/CHANGELOG.md +++ b/packages/go_router/CHANGELOG.md @@ -1,3 +1,7 @@ +## 9.0.3 + +- Adds helpers for go_router_builder for StatefulShellRoute support + ## 9.0.2 - Exposes package-level privates. diff --git a/packages/go_router/lib/src/route.dart b/packages/go_router/lib/src/route.dart index 71aef137be3..917aaa7287e 100644 --- a/packages/go_router/lib/src/route.dart +++ b/packages/go_router/lib/src/route.dart @@ -744,8 +744,8 @@ class StatefulShellRoute extends ShellRouteBase { super.parentNavigatorKey, this.restorationScopeId, }) : assert(branches.isNotEmpty), - assert((pageBuilder != null) ^ (builder != null), - 'One of builder or pageBuilder must be provided, but not both'), + assert((pageBuilder != null) || (builder != null), + 'One of builder or pageBuilder must be provided'), assert(_debugUniqueNavigatorKeys(branches).length == branches.length, 'Navigator keys must be unique'), assert(_debugValidateParentNavigatorKeys(branches)), diff --git a/packages/go_router/lib/src/route_data.dart b/packages/go_router/lib/src/route_data.dart index c0053a06b6c..2fedb32420f 100644 --- a/packages/go_router/lib/src/route_data.dart +++ b/packages/go_router/lib/src/route_data.dart @@ -140,7 +140,7 @@ abstract class ShellRouteData extends RouteData { ) => const NoOpPage(); - /// [pageBuilder] is used to build the page + /// [builder] is used to build the widget Widget builder( BuildContext context, GoRouterState state, @@ -157,16 +157,10 @@ abstract class ShellRouteData extends RouteData { required T Function(GoRouterState) factory, GlobalKey? navigatorKey, List routes = const [], + List? observers, + String? restorationScopeId, }) { 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; } @@ -197,6 +191,8 @@ abstract class ShellRouteData extends RouteData { pageBuilder: pageBuilder, routes: routes, navigatorKey: navigatorKey, + observers: observers, + restorationScopeId: restorationScopeId, ); } @@ -208,6 +204,116 @@ abstract class ShellRouteData extends RouteData { ); } +/// Base class for supporting +/// [StatefulShellRoute](https://pub.dev/documentation/go_router/latest/go_router/StatefulShellRoute-class.html) +abstract class StatefulShellRouteData extends RouteData { + /// Default const constructor + const StatefulShellRouteData(); + + /// [pageBuilder] is used to build the page + Page pageBuilder( + BuildContext context, + GoRouterState state, + StatefulNavigationShell navigationShell, + ) => + const NoOpPage(); + + /// [builder] is used to build the widget + Widget builder( + BuildContext context, + GoRouterState state, + StatefulNavigationShell navigationShell, + ) => + throw UnimplementedError( + 'One of `builder` or `pageBuilder` must be implemented.', + ); + + /// A helper function used by generated code. + /// + /// Should not be used directly. + static StatefulShellRoute $route({ + required T Function(GoRouterState) factory, + required List branches, + ShellNavigationContainerBuilder? navigatorContainerBuilder, + String? restorationScopeId, + }) { + T factoryImpl(GoRouterState state) { + return (_stateObjectExpando[state] ??= factory(state)) as T; + } + + Widget builder( + BuildContext context, + GoRouterState state, + StatefulNavigationShell navigationShell, + ) => + factoryImpl(state).builder( + context, + state, + navigationShell, + ); + + Page pageBuilder( + BuildContext context, + GoRouterState state, + StatefulNavigationShell navigationShell, + ) => + factoryImpl(state).pageBuilder( + context, + state, + navigationShell, + ); + + if (navigatorContainerBuilder != null) { + return StatefulShellRoute( + branches: branches, + builder: builder, + pageBuilder: pageBuilder, + navigatorContainerBuilder: navigatorContainerBuilder, + restorationScopeId: restorationScopeId, + ); + } + return StatefulShellRoute.indexedStack( + branches: branches, + builder: builder, + pageBuilder: pageBuilder, + restorationScopeId: restorationScopeId, + ); + } + + /// Used to cache [StatefulShellRouteData] that corresponds to a given [GoRouterState] + /// to minimize the number of times it has to be deserialized. + static final Expando _stateObjectExpando = + Expando( + 'GoRouteState to StatefulShellRouteData expando', + ); +} + +/// Base class for supporting +/// [StatefulShellRoute](https://pub.dev/documentation/go_router/latest/go_router/StatefulShellRoute-class.html) +abstract class StatefulShellBranchData { + /// Default const constructor + const StatefulShellBranchData(); + + /// A helper function used by generated code. + /// + /// Should not be used directly. + static StatefulShellBranch $branch({ + GlobalKey? navigatorKey, + List routes = const [], + List? observers, + String? initialLocation, + String? restorationScopeId, + }) { + return StatefulShellBranch( + routes: routes, + navigatorKey: navigatorKey, + observers: observers, + initialLocation: initialLocation, + restorationScopeId: restorationScopeId, + ); + } +} + /// A superclass for each typed route descendant class TypedRoute { /// Default const constructor @@ -259,6 +365,35 @@ class TypedShellRoute extends TypedRoute { final List> routes; } +/// A superclass for each typed shell route descendant +@Target({TargetKind.library, TargetKind.classType}) +class TypedStatefulShellRoute + extends TypedRoute { + /// Default const constructor + const TypedStatefulShellRoute({ + this.branches = const >[], + }); + + /// Child route definitions. + /// + /// See [RouteBase.routes]. + final List> branches; +} + +/// A superclass for each typed shell route descendant +@Target({TargetKind.library, TargetKind.classType}) +class TypedStatefulShellBranch { + /// Default const constructor + const TypedStatefulShellBranch({ + 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. @internal class NoOpPage extends Page { diff --git a/packages/go_router/pubspec.yaml b/packages/go_router/pubspec.yaml index f6c8fe43a19..2730a7a892d 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: 9.0.2 +version: 9.0.3 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 43299ec5402..684c6c3bcbb 100644 --- a/packages/go_router/test/route_data_test.dart +++ b/packages/go_router/test/route_data_test.dart @@ -86,6 +86,68 @@ final ShellRoute _shellRouteDataPageBuilder = ShellRouteData.$route( ], ); +class _StatefulShellRouteDataBuilder extends StatefulShellRouteData { + const _StatefulShellRouteDataBuilder(); + + @override + Widget builder( + BuildContext context, + GoRouterState state, + StatefulNavigationShell navigator, + ) => + SizedBox( + key: const Key('builder'), + child: navigator, + ); +} + +final StatefulShellRoute _statefulShellRouteDataBuilder = + StatefulShellRouteData.$route( + factory: (GoRouterState state) => const _StatefulShellRouteDataBuilder(), + branches: [ + StatefulShellBranchData.$branch( + routes: [ + GoRouteData.$route( + path: '/child', + factory: (GoRouterState state) => const _GoRouteDataBuild(), + ), + ], + ), + ], +); + +class _StatefulShellRouteDataPageBuilder extends StatefulShellRouteData { + const _StatefulShellRouteDataPageBuilder(); + + @override + Page pageBuilder( + BuildContext context, + GoRouterState state, + StatefulNavigationShell navigator, + ) => + MaterialPage( + child: SizedBox( + key: const Key('page-builder'), + child: navigator, + ), + ); +} + +final StatefulShellRoute _statefulShellRouteDataPageBuilder = + StatefulShellRouteData.$route( + factory: (GoRouterState state) => const _StatefulShellRouteDataPageBuilder(), + branches: [ + StatefulShellBranchData.$branch( + routes: [ + GoRouteData.$route( + path: '/child', + factory: (GoRouterState state) => const _GoRouteDataBuild(), + ), + ], + ), + ], +); + class _GoRouteDataRedirectPage extends GoRouteData { const _GoRouteDataRedirectPage(); @override @@ -113,11 +175,7 @@ void main() { initialLocation: '/build', routes: _routes, ); - await tester.pumpWidget(MaterialApp.router( - routeInformationProvider: goRouter.routeInformationProvider, - routeInformationParser: goRouter.routeInformationParser, - routerDelegate: goRouter.routerDelegate, - )); + await tester.pumpWidget(MaterialApp.router(routerConfig: goRouter)); expect(find.byKey(const Key('build')), findsOneWidget); expect(find.byKey(const Key('buildPage')), findsNothing); }, @@ -130,11 +188,7 @@ void main() { initialLocation: '/build-page', routes: _routes, ); - await tester.pumpWidget(MaterialApp.router( - routeInformationProvider: goRouter.routeInformationProvider, - routeInformationParser: goRouter.routeInformationParser, - routerDelegate: goRouter.routerDelegate, - )); + await tester.pumpWidget(MaterialApp.router(routerConfig: goRouter)); expect(find.byKey(const Key('build')), findsNothing); expect(find.byKey(const Key('buildPage')), findsOneWidget); }, @@ -151,11 +205,7 @@ void main() { _shellRouteDataBuilder, ], ); - await tester.pumpWidget(MaterialApp.router( - routeInformationProvider: goRouter.routeInformationProvider, - routeInformationParser: goRouter.routeInformationParser, - routerDelegate: goRouter.routerDelegate, - )); + await tester.pumpWidget(MaterialApp.router(routerConfig: goRouter)); expect(find.byKey(const Key('builder')), findsOneWidget); expect(find.byKey(const Key('page-builder')), findsNothing); }, @@ -170,11 +220,39 @@ void main() { _shellRouteDataPageBuilder, ], ); - await tester.pumpWidget(MaterialApp.router( - routeInformationProvider: goRouter.routeInformationProvider, - routeInformationParser: goRouter.routeInformationParser, - routerDelegate: goRouter.routerDelegate, - )); + await tester.pumpWidget(MaterialApp.router(routerConfig: goRouter)); + expect(find.byKey(const Key('builder')), findsNothing); + expect(find.byKey(const Key('page-builder')), findsOneWidget); + }, + ); + }); + + group('StatefulShellRouteData', () { + testWidgets( + 'It should build the page from the overridden build method', + (WidgetTester tester) async { + final GoRouter goRouter = GoRouter( + initialLocation: '/child', + routes: [ + _statefulShellRouteDataBuilder, + ], + ); + await tester.pumpWidget(MaterialApp.router(routerConfig: goRouter)); + 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: [ + _statefulShellRouteDataPageBuilder, + ], + ); + await tester.pumpWidget(MaterialApp.router(routerConfig: goRouter)); expect(find.byKey(const Key('builder')), findsNothing); expect(find.byKey(const Key('page-builder')), findsOneWidget); }, @@ -188,9 +266,7 @@ void main() { initialLocation: '/redirect', routes: _routes, ); - await tester.pumpWidget(MaterialApp.router( - routerConfig: goRouter, - )); + await tester.pumpWidget(MaterialApp.router(routerConfig: goRouter)); expect(find.byKey(const Key('build')), findsNothing); expect(find.byKey(const Key('buildPage')), findsOneWidget); }, @@ -203,9 +279,7 @@ void main() { initialLocation: '/redirect-with-state', routes: _routes, ); - await tester.pumpWidget(MaterialApp.router( - routerConfig: goRouter, - )); + await tester.pumpWidget(MaterialApp.router(routerConfig: goRouter)); expect(find.byKey(const Key('build')), findsNothing); expect(find.byKey(const Key('buildPage')), findsNothing); },