Skip to content

[go_router] implemented helpers for StatefulShellRoute #4228

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 10 commits into from
Jul 7, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions packages/go_router/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
## 9.0.3

- Adds helpers for go_router_builder for StatefulShellRoute support

## 9.0.2

- Exposes package-level privates.
Expand Down
4 changes: 2 additions & 2 deletions packages/go_router/lib/src/route.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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)),
Expand Down
153 changes: 144 additions & 9 deletions packages/go_router/lib/src/route_data.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -157,16 +157,10 @@ abstract class ShellRouteData extends RouteData {
required T Function(GoRouterState) factory,
GlobalKey<NavigatorState>? navigatorKey,
List<RouteBase> routes = const <RouteBase>[],
List<NavigatorObserver>? 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;
}

Expand Down Expand Up @@ -197,6 +191,8 @@ abstract class ShellRouteData extends RouteData {
pageBuilder: pageBuilder,
routes: routes,
navigatorKey: navigatorKey,
observers: observers,
restorationScopeId: restorationScopeId,
);
}

Expand All @@ -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<void> 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<T extends StatefulShellRouteData>({
required T Function(GoRouterState) factory,
required List<StatefulShellBranch> 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<void> 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,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this should also be in line 260, also how would one specify this in TypeStatefulShellRoute?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we should also consider adding this to TypedShellRoute

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

);
}

/// Used to cache [StatefulShellRouteData] that corresponds to a given [GoRouterState]
/// to minimize the number of times it has to be deserialized.
static final Expando<StatefulShellRouteData> _stateObjectExpando =
Expando<StatefulShellRouteData>(
'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<T extends StatefulShellBranchData>({
GlobalKey<NavigatorState>? navigatorKey,
List<RouteBase> routes = const <RouteBase>[],
List<NavigatorObserver>? observers,
String? initialLocation,
String? restorationScopeId,
}) {
return StatefulShellBranch(
routes: routes,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

how does one specify initialLocation and restorationScopeId? we may also want a way to specify observers

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

navigatorKey: navigatorKey,
observers: observers,
initialLocation: initialLocation,
restorationScopeId: restorationScopeId,
);
}
}

/// A superclass for each typed route descendant
class TypedRoute<T extends RouteData> {
/// Default const constructor
Expand Down Expand Up @@ -259,6 +365,35 @@ class TypedShellRoute<T extends ShellRouteData> extends TypedRoute<T> {
final List<TypedRoute<RouteData>> routes;
}

/// A superclass for each typed shell route descendant
@Target(<TargetKind>{TargetKind.library, TargetKind.classType})
class TypedStatefulShellRoute<T extends StatefulShellRouteData>
extends TypedRoute<T> {
/// Default const constructor
const TypedStatefulShellRoute({
this.branches = const <TypedStatefulShellBranch<StatefulShellBranchData>>[],
});

/// Child route definitions.
///
/// See [RouteBase.routes].
final List<TypedStatefulShellBranch<StatefulShellBranchData>> branches;
}

/// A superclass for each typed shell route descendant
@Target(<TargetKind>{TargetKind.library, TargetKind.classType})
class TypedStatefulShellBranch<T extends StatefulShellBranchData> {
/// Default const constructor
const TypedStatefulShellBranch({
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.
@internal
class NoOpPage extends Page<void> {
Expand Down
2 changes: 1 addition & 1 deletion packages/go_router/pubspec.yaml
Original file line number Diff line number Diff line change
@@ -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

Expand Down
126 changes: 100 additions & 26 deletions packages/go_router/test/route_data_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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: <StatefulShellBranch>[
StatefulShellBranchData.$branch(
routes: <RouteBase>[
GoRouteData.$route(
path: '/child',
factory: (GoRouterState state) => const _GoRouteDataBuild(),
),
],
),
],
);

class _StatefulShellRouteDataPageBuilder extends StatefulShellRouteData {
const _StatefulShellRouteDataPageBuilder();

@override
Page<void> pageBuilder(
BuildContext context,
GoRouterState state,
StatefulNavigationShell navigator,
) =>
MaterialPage<void>(
child: SizedBox(
key: const Key('page-builder'),
child: navigator,
),
);
}

final StatefulShellRoute _statefulShellRouteDataPageBuilder =
StatefulShellRouteData.$route(
factory: (GoRouterState state) => const _StatefulShellRouteDataPageBuilder(),
branches: <StatefulShellBranch>[
StatefulShellBranchData.$branch(
routes: <RouteBase>[
GoRouteData.$route(
path: '/child',
factory: (GoRouterState state) => const _GoRouteDataBuild(),
),
],
),
],
);

class _GoRouteDataRedirectPage extends GoRouteData {
const _GoRouteDataRedirectPage();
@override
Expand Down Expand Up @@ -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);
},
Expand All @@ -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);
},
Expand All @@ -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);
},
Expand All @@ -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: <RouteBase>[
_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: <RouteBase>[
_statefulShellRouteDataPageBuilder,
],
);
await tester.pumpWidget(MaterialApp.router(routerConfig: goRouter));
expect(find.byKey(const Key('builder')), findsNothing);
expect(find.byKey(const Key('page-builder')), findsOneWidget);
},
Expand All @@ -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);
},
Expand All @@ -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);
},
Expand Down