diff --git a/packages/go_router/example/lib/others/custom_stateful_shell_route.dart b/packages/go_router/example/lib/others/custom_stateful_shell_route.dart index 5af1504234a..0a66721fd2b 100644 --- a/packages/go_router/example/lib/others/custom_stateful_shell_route.dart +++ b/packages/go_router/example/lib/others/custom_stateful_shell_route.dart @@ -10,6 +10,9 @@ final GlobalKey _rootNavigatorKey = GlobalKey(debugLabel: 'root'); final GlobalKey _tabANavigatorKey = GlobalKey(debugLabel: 'tabANav'); +final GlobalKey _tabBNavigatorKey = + GlobalKey(debugLabel: 'tabBNav'); +final GlobalKey _shellKey = GlobalKey(debugLabel: 'shellKey'); // This example demonstrates how to setup nested navigation using a // BottomNavigationBar, where each bar item uses its own persistent navigator, @@ -33,25 +36,22 @@ class NestedTabNavigationExampleApp extends StatelessWidget { initialLocation: '/a', routes: [ StatefulShellRoute( - builder: (BuildContext context, GoRouterState state, - StatefulNavigationShell navigationShell) { - // This nested StatefulShellRoute demonstrates the use of a - // custom container for the branch Navigators. In this implementation, - // no customization is done in the builder function (navigationShell - // itself is simply used as the Widget for the route). Instead, the - // navigatorContainerBuilder function below is provided to - // customize the container for the branch Navigators. - return navigationShell; - }, - navigatorContainerBuilder: (BuildContext context, - StatefulNavigationShell navigationShell, List children) { + // To ensure state is maintained even when route configurations is + // reloaded, provide a global key to the ShellRoute. + key: _shellKey, + name: 'rootShell', + // This nested StatefulShellRoute demonstrates the use of a custom + // container for the branch Navigators, using the + // `navigatorContainerBuilder` parameter. When doing so, the `builder` + // should not be provided, and `pageBuilder` is optional. + navigatorContainerBuilder: (BuildContext context, ShellRouteState state, + List children) { // Returning a customized container for the branch // Navigators (i.e. the `List children` argument). // // See ScaffoldWithNavBar for more details on how the children // are managed (using AnimatedBranchContainer). - return ScaffoldWithNavBar( - navigationShell: navigationShell, children: children); + return ScaffoldWithNavBar(shellState: state, children: children); }, branches: [ // The route branch for the first tab of the bottom navigation bar. @@ -80,6 +80,7 @@ class NestedTabNavigationExampleApp extends StatelessWidget { // The route branch for the third tab of the bottom navigation bar. StatefulShellBranch( + navigatorKey: _tabBNavigatorKey, // StatefulShellBranch will automatically use the first descendant // GoRoute as the initial location of the branch. If another route // is desired, specify the location of it using the defaultLocation @@ -87,22 +88,16 @@ class NestedTabNavigationExampleApp extends StatelessWidget { // defaultLocation: '/c2', routes: [ StatefulShellRoute( - builder: (BuildContext context, GoRouterState state, - StatefulNavigationShell navigationShell) { - // Just like with the top level StatefulShellRoute, no - // customization is done in the builder function. - return navigationShell; - }, + name: 'nestedShell', navigatorContainerBuilder: (BuildContext context, - StatefulNavigationShell navigationShell, - List children) { + ShellRouteState state, List children) { // Returning a customized container for the branch // Navigators (i.e. the `List children` argument). // // See TabbedRootScreen for more details on how the children // are managed (in a TabBarView). return TabbedRootScreen( - navigationShell: navigationShell, children: children); + shellState: state, children: children); }, // This bottom tab uses a nested shell, wrapping sub routes in a // top TabBar. @@ -171,13 +166,13 @@ class NestedTabNavigationExampleApp extends StatelessWidget { class ScaffoldWithNavBar extends StatelessWidget { /// Constructs an [ScaffoldWithNavBar]. const ScaffoldWithNavBar({ - required this.navigationShell, + required this.shellState, required this.children, Key? key, }) : super(key: key ?? const ValueKey('ScaffoldWithNavBar')); /// The navigation shell and container for the branch Navigators. - final StatefulNavigationShell navigationShell; + final ShellRouteState shellState; /// The children (branch Navigators) to display in a custom container /// ([AnimatedBranchContainer]). @@ -187,7 +182,7 @@ class ScaffoldWithNavBar extends StatelessWidget { Widget build(BuildContext context) { return Scaffold( body: AnimatedBranchContainer( - currentIndex: navigationShell.currentIndex, + currentIndex: shellState.navigatorIndex, children: children, ), bottomNavigationBar: BottomNavigationBar( @@ -199,7 +194,7 @@ class ScaffoldWithNavBar extends StatelessWidget { BottomNavigationBarItem(icon: Icon(Icons.home), label: 'Section A'), BottomNavigationBarItem(icon: Icon(Icons.work), label: 'Section B'), ], - currentIndex: navigationShell.currentIndex, + currentIndex: shellState.navigatorIndex, onTap: (int index) => _onTap(context, index), ), ); @@ -208,17 +203,13 @@ class ScaffoldWithNavBar extends StatelessWidget { /// Navigate to the current location of the branch at the provided index when /// tapping an item in the BottomNavigationBar. void _onTap(BuildContext context, int index) { - // When navigating to a new branch, it's recommended to use the goBranch - // method, as doing so makes sure the last navigation state of the - // Navigator for the branch is restored. - navigationShell.goBranch( - index, - // A common pattern when using bottom navigation bars is to support - // navigating to the initial location when tapping the item that is - // already active. This example demonstrates how to support this behavior, - // using the initialLocation parameter of goBranch. - initialLocation: index == navigationShell.currentIndex, - ); + // A common pattern when using bottom navigation bars is to support + // navigating to the initial location when tapping the item that is + // already active. + if (index == shellState.navigatorIndex) { + shellState.resetNavigatorLocation(index); + } + context.restore(shellState.navigatorLocation(index)); } } @@ -377,10 +368,10 @@ class DetailsScreenState extends State { class TabbedRootScreen extends StatefulWidget { /// Constructs a TabbedRootScreen const TabbedRootScreen( - {required this.navigationShell, required this.children, super.key}); + {required this.shellState, required this.children, super.key}); /// The current state of the parent StatefulShellRoute. - final StatefulNavigationShell navigationShell; + final ShellRouteState shellState; /// The children (branch Navigators) to display in the [TabBarView]. final List children; @@ -391,15 +382,17 @@ class TabbedRootScreen extends StatefulWidget { class _TabbedRootScreenState extends State with SingleTickerProviderStateMixin { + ShellRouteState get _shellState => widget.shellState; + late final TabController _tabController = TabController( length: widget.children.length, vsync: this, - initialIndex: widget.navigationShell.currentIndex); + initialIndex: _shellState.navigatorIndex); @override void didUpdateWidget(covariant TabbedRootScreen oldWidget) { super.didUpdateWidget(oldWidget); - _tabController.index = widget.navigationShell.currentIndex; + _tabController.index = _shellState.navigatorIndex; } @override @@ -424,7 +417,7 @@ class _TabbedRootScreenState extends State } void _onTabTap(BuildContext context, int index) { - widget.navigationShell.goBranch(index); + context.restore(_shellState.navigatorLocation(index)); } } @@ -446,6 +439,10 @@ class TabScreen extends StatelessWidget { mainAxisSize: MainAxisSize.min, children: [ Text('Screen $label', style: Theme.of(context).textTheme.titleLarge), + Text( + 'Shell navigator index: ${ShellRouteState.of(context).navigatorIndex}'), + Text( + 'Root shell navigator index: ${ShellRouteState.of(context, name: 'rootShell').navigatorIndex}'), const Padding(padding: EdgeInsets.all(4)), TextButton( onPressed: () { diff --git a/packages/go_router/example/lib/others/stateful_shell_state_restoration.dart b/packages/go_router/example/lib/others/stateful_shell_state_restoration.dart index aeecd115590..2f22b0ee64f 100644 --- a/packages/go_router/example/lib/others/stateful_shell_state_restoration.dart +++ b/packages/go_router/example/lib/others/stateful_shell_state_restoration.dart @@ -19,11 +19,11 @@ class RestorableStatefulShellRouteExampleApp extends StatelessWidget { routes: [ StatefulShellRoute.indexedStack( restorationScopeId: 'shell1', - pageBuilder: (BuildContext context, GoRouterState state, - StatefulNavigationShell navigationShell) { + pageBuilder: + (BuildContext context, ShellRouteState state, Widget child) { return MaterialPage( restorationId: 'shellWidget1', - child: ScaffoldWithNavBar(navigationShell: navigationShell)); + child: ScaffoldWithNavBar(shellState: state, child: child)); }, branches: [ // The route branch for the first tab of the bottom navigation bar. @@ -105,27 +105,35 @@ class RestorableStatefulShellRouteExampleApp extends StatelessWidget { class ScaffoldWithNavBar extends StatelessWidget { /// Constructs an [ScaffoldWithNavBar]. const ScaffoldWithNavBar({ - required this.navigationShell, + required this.shellState, + required this.child, Key? key, }) : super(key: key ?? const ValueKey('ScaffoldWithNavBar')); + /// The state of the shell route. + final ShellRouteState shellState; + /// The navigation shell and container for the branch Navigators. - final StatefulNavigationShell navigationShell; + final Widget child; @override Widget build(BuildContext context) { return Scaffold( - body: navigationShell, + body: child, bottomNavigationBar: BottomNavigationBar( items: const [ BottomNavigationBarItem(icon: Icon(Icons.home), label: 'Section A'), BottomNavigationBarItem(icon: Icon(Icons.work), label: 'Section B'), ], - currentIndex: navigationShell.currentIndex, - onTap: (int tappedIndex) => navigationShell.goBranch(tappedIndex), + currentIndex: shellState.navigatorIndex, + onTap: (int tappedIndex) => _onTabTap(context, tappedIndex), ), ); } + + void _onTabTap(BuildContext context, int index) { + context.restore(shellState.navigatorLocation(index)); + } } /// Widget for the root/initial pages in the bottom navigation bar. diff --git a/packages/go_router/example/lib/stateful_shell_route.dart b/packages/go_router/example/lib/stateful_shell_route.dart index e6f0e7a1c0c..aaa8581811e 100644 --- a/packages/go_router/example/lib/stateful_shell_route.dart +++ b/packages/go_router/example/lib/stateful_shell_route.dart @@ -30,13 +30,12 @@ class NestedTabNavigationExampleApp extends StatelessWidget { routes: [ // #docregion configuration-builder StatefulShellRoute.indexedStack( - builder: (BuildContext context, GoRouterState state, - StatefulNavigationShell navigationShell) { + builder: (BuildContext context, ShellRouteState state, Widget child) { // Return the widget that implements the custom shell (in this case // using a BottomNavigationBar). The StatefulNavigationShell is passed // to be able access the state of the shell and to navigate to other // branches in a stateful way. - return ScaffoldWithNavBar(navigationShell: navigationShell); + return ScaffoldWithNavBar(shellState: state, child: child); }, // #enddocregion configuration-builder // #docregion configuration-branches @@ -142,12 +141,16 @@ class NestedTabNavigationExampleApp extends StatelessWidget { class ScaffoldWithNavBar extends StatelessWidget { /// Constructs an [ScaffoldWithNavBar]. const ScaffoldWithNavBar({ - required this.navigationShell, + required this.shellState, + required this.child, Key? key, }) : super(key: key ?? const ValueKey('ScaffoldWithNavBar')); + /// The state of the shell route. + final ShellRouteState shellState; + /// The navigation shell and container for the branch Navigators. - final StatefulNavigationShell navigationShell; + final Widget child; // #docregion configuration-custom-shell @override @@ -155,42 +158,39 @@ class ScaffoldWithNavBar extends StatelessWidget { return Scaffold( // The StatefulNavigationShell from the associated StatefulShellRoute is // directly passed as the body of the Scaffold. - body: navigationShell, + body: child, bottomNavigationBar: BottomNavigationBar( - // Here, the items of BottomNavigationBar are hard coded. In a real - // world scenario, the items would most likely be generated from the - // branches of the shell route, which can be fetched using - // `navigationShell.route.branches`. - items: const [ - BottomNavigationBarItem(icon: Icon(Icons.home), label: 'Section A'), - BottomNavigationBarItem(icon: Icon(Icons.work), label: 'Section B'), - BottomNavigationBarItem(icon: Icon(Icons.tab), label: 'Section C'), - ], - currentIndex: navigationShell.currentIndex, - // Navigate to the current location of the branch at the provided index - // when tapping an item in the BottomNavigationBar. - onTap: (int index) => navigationShell.goBranch(index), - ), + // Here, the items of BottomNavigationBar are hard coded. In a real + // world scenario, the items would most likely be generated from the + // branches of the shell route, which can be fetched using + // `navigationShell.route.branches`. + items: const [ + BottomNavigationBarItem(icon: Icon(Icons.home), label: 'Section A'), + BottomNavigationBarItem(icon: Icon(Icons.work), label: 'Section B'), + BottomNavigationBarItem(icon: Icon(Icons.tab), label: 'Section C'), + ], + currentIndex: shellState.navigatorIndex, + // Navigate to the current location of the branch at the provided index + // when tapping an item in the BottomNavigationBar. + onTap: (int index) { + context.restore(shellState.navigatorLocation(index)); + }), ); } // #enddocregion configuration-custom-shell /// NOTE: For a slightly more sophisticated branch switching, change the onTap /// handler on the BottomNavigationBar above to the following: - /// `onTap: (int index) => _onTap(context, index),` + /// `onTap: _onTap,` // ignore: unused_element void _onTap(BuildContext context, int index) { - // When navigating to a new branch, it's recommended to use the goBranch - // method, as doing so makes sure the last navigation state of the - // Navigator for the branch is restored. - navigationShell.goBranch( - index, - // A common pattern when using bottom navigation bars is to support - // navigating to the initial location when tapping the item that is - // already active. This example demonstrates how to support this behavior, - // using the initialLocation parameter of goBranch. - initialLocation: index == navigationShell.currentIndex, - ); + // A common pattern when using bottom navigation bars is to support + // navigating to the initial location when tapping the item that is + // already active. + if (index == shellState.navigatorIndex) { + shellState.resetNavigatorLocation(index); + } + context.restore(shellState.navigatorLocation(index)); } } @@ -217,7 +217,8 @@ class RootScreen extends StatelessWidget { Widget build(BuildContext context) { return Scaffold( appBar: AppBar( - title: Text('Root of section $label'), + title: Text( + 'Root of section $label - navigatorIndex: ${ShellRouteState.of(context).navigatorIndex}'), ), body: Center( child: Column( diff --git a/packages/go_router/lib/src/builder.dart b/packages/go_router/lib/src/builder.dart index 49b6372c693..e8afe2c7e12 100644 --- a/packages/go_router/lib/src/builder.dart +++ b/packages/go_router/lib/src/builder.dart @@ -109,7 +109,9 @@ class RouteBuilder { return builderWithNav( context, _CustomNavigator( + parentNavigatorKey: null, navigatorKey: configuration.navigatorKey, + shellRouteState: null, observers: observers, navigatorRestorationId: restorationScopeId, onPopPageWithRouteMatch: onPopPageWithRouteMatch, @@ -126,7 +128,9 @@ class RouteBuilder { class _CustomNavigator extends StatefulWidget { const _CustomNavigator({ super.key, + required this.parentNavigatorKey, required this.navigatorKey, + required this.shellRouteState, required this.observers, required this.navigatorRestorationId, required this.onPopPageWithRouteMatch, @@ -137,7 +141,9 @@ class _CustomNavigator extends StatefulWidget { required this.errorPageBuilder, }); + final GlobalKey? parentNavigatorKey; final GlobalKey navigatorKey; + final ShellRouteState? shellRouteState; final List observers; /// The actual [RouteMatchBase]s to be built. @@ -160,7 +166,8 @@ class _CustomNavigator extends StatefulWidget { class _CustomNavigatorState extends State<_CustomNavigator> { HeroController? _controller; late Map, RouteMatchBase> _pageToRouteMatchBase; - final GoRouterStateRegistry _registry = GoRouterStateRegistry(); + late final GoRouterStateRegistry _registry = + GoRouterStateRegistry(widget.parentNavigatorKey, widget.shellRouteState); List>? _pages; @override @@ -263,34 +270,31 @@ class _CustomNavigatorState extends State<_CustomNavigator> { BuildContext context, ShellRouteMatch match, ) { - final GoRouterState state = - match.buildState(widget.configuration, widget.matchList); + final ShellRouteState state = match.buildState( + widget.configuration, widget.matchList) as ShellRouteState; final GlobalKey navigatorKey = match.navigatorKey; - final ShellRouteContext shellRouteContext = ShellRouteContext( - route: match.route, - routerState: state, - navigatorKey: navigatorKey, - routeMatchList: widget.matchList, - navigatorBuilder: - (List? observers, String? restorationScopeId) { - return _CustomNavigator( - // The state needs to persist across rebuild. - key: GlobalObjectKey(navigatorKey.hashCode), - navigatorRestorationId: restorationScopeId, - navigatorKey: navigatorKey, - matches: match.matches, - matchList: widget.matchList, - configuration: widget.configuration, - observers: observers ?? const [], - onPopPageWithRouteMatch: widget.onPopPageWithRouteMatch, - // This is used to recursively build pages under this shell route. - errorBuilder: widget.errorBuilder, - errorPageBuilder: widget.errorPageBuilder, - ); - }, - ); + Widget navigatorBuilder( + List? observers, String? restorationScopeId) { + return _CustomNavigator( + // The state needs to persist across rebuild. + key: GlobalObjectKey(navigatorKey.hashCode), + parentNavigatorKey: widget.navigatorKey, + shellRouteState: state, + navigatorRestorationId: restorationScopeId, + navigatorKey: navigatorKey, + matches: match.matches, + matchList: widget.matchList, + configuration: widget.configuration, + observers: observers ?? const [], + onPopPageWithRouteMatch: widget.onPopPageWithRouteMatch, + // This is used to recursively build pages under this shell route. + errorBuilder: widget.errorBuilder, + errorPageBuilder: widget.errorPageBuilder, + ); + } + final Page? page = - match.route.buildPage(context, state, shellRouteContext); + match.route.buildPage(context, state, navigatorBuilder); if (page != null && page is! NoOpPage) { return page; } @@ -301,7 +305,7 @@ class _CustomNavigatorState extends State<_CustomNavigator> { state, Builder( builder: (BuildContext context) { - return match.route.buildWidget(context, state, shellRouteContext)!; + return match.route.buildWidget(context, state, navigatorBuilder)!; }, ), ); diff --git a/packages/go_router/lib/src/configuration.dart b/packages/go_router/lib/src/configuration.dart index 8be984ecb6f..23ddc2b4b57 100644 --- a/packages/go_router/lib/src/configuration.dart +++ b/packages/go_router/lib/src/configuration.dart @@ -84,24 +84,19 @@ class RouteConfiguration { ], ); } - } else if (route is ShellRoute) { - _debugCheckParentNavigatorKeys( - route.routes, - >[...allowedKeys..add(route.navigatorKey)], - ); - } else if (route is StatefulShellRoute) { - for (final StatefulShellBranch branch in route.branches) { + } else if (route is ShellRouteBase) { + for (final GlobalKey key in route.nestedNavigatorKeys) { assert( - !allowedKeys.contains(branch.navigatorKey), - 'StatefulShellBranch must not reuse an ancestor navigatorKey ' - '(${branch.navigatorKey})'); - + !allowedKeys.contains(key), + 'Shell route must not reuse an ancestor navigatorKey ' + '($key)'); + } + for (final RouteBase subRoute in route.routes) { + final GlobalKey key = + route.navigatorKeyForSubRoute(subRoute); _debugCheckParentNavigatorKeys( - branch.routes, - >[ - ...allowedKeys, - branch.navigatorKey, - ], + [subRoute], + >[...allowedKeys, key], ); } } @@ -133,43 +128,8 @@ class RouteConfiguration { // points to a descendant route of the route branch. bool _debugCheckStatefulShellBranchDefaultLocations(List routes) { for (final RouteBase route in routes) { - if (route is StatefulShellRoute) { - for (final StatefulShellBranch branch in route.branches) { - if (branch.initialLocation == null) { - // Recursively search for the first GoRoute descendant. Will - // throw assertion error if not found. - final GoRoute? route = branch.defaultRoute; - final String? initialLocation = - route != null ? locationForRoute(route) : null; - assert( - initialLocation != null, - 'The default location of a StatefulShellBranch must be ' - 'derivable from GoRoute descendant'); - assert( - route!.pathParameters.isEmpty, - 'The default location of a StatefulShellBranch cannot be ' - 'a parameterized route'); - } else { - final RouteMatchList matchList = - findMatch(Uri.parse(branch.initialLocation!)); - assert( - !matchList.isError, - 'initialLocation (${matchList.uri}) of StatefulShellBranch must ' - 'be a valid location'); - final List matchRoutes = matchList.routes; - final int shellIndex = matchRoutes.indexOf(route); - bool matchFound = false; - if (shellIndex >= 0 && (shellIndex + 1) < matchRoutes.length) { - final RouteBase branchRoot = matchRoutes[shellIndex + 1]; - matchFound = branch.routes.contains(branchRoot); - } - assert( - matchFound, - 'The initialLocation (${branch.initialLocation}) of ' - 'StatefulShellBranch must match a descendant route of the ' - 'branch'); - } - } + if (route is ShellRouteBase) { + route.debugValidateNestedNavigators(this); } _debugCheckStatefulShellBranchDefaultLocations(route.routes); } diff --git a/packages/go_router/lib/src/information_provider.dart b/packages/go_router/lib/src/information_provider.dart index dc979193b32..f3eedba4376 100644 --- a/packages/go_router/lib/src/information_provider.dart +++ b/packages/go_router/lib/src/information_provider.dart @@ -32,6 +32,9 @@ enum NavigatingType { /// Restore the current match list with /// [RouteInformationState.baseRouteMatchList]. + // TODO(tolo): Remove this in next major release. + @Deprecated( + 'Configure path on StatefulShellRoute and use GoRouter.go instead.') restore, } @@ -173,6 +176,9 @@ class GoRouteInformationProvider extends RouteInformationProvider } /// Restores the current route matches with the `matchList`. + // TODO(tolo): Remove this in next major release. + @Deprecated( + 'Configure path on StatefulShellRoute and use GoRouter.go instead.') void restore(String location, {required RouteMatchList matchList}) { _setValue( matchList.uri.toString(), diff --git a/packages/go_router/lib/src/match.dart b/packages/go_router/lib/src/match.dart index 4d24416284e..8b0e4e96c66 100644 --- a/packages/go_router/lib/src/match.dart +++ b/packages/go_router/lib/src/match.dart @@ -375,15 +375,19 @@ class ShellRouteMatch extends RouteMatchBase { if (leafMatch is ImperativeRouteMatch) { matches = leafMatch.matches; } - return GoRouterState( + return ShellRouteState( configuration, uri: matches.uri, matchedLocation: matchedLocation, + name: route.name, fullPath: matches.fullPath, pathParameters: matches.pathParameters, pageKey: pageKey, extra: matches.extra, topRoute: matches.lastOrNull?.route, + navigatorKey: navigatorKey, + shellRoute: route, + routeMatchList: matches, ); } diff --git a/packages/go_router/lib/src/misc/extensions.dart b/packages/go_router/lib/src/misc/extensions.dart index c137022b802..b49d9942e15 100644 --- a/packages/go_router/lib/src/misc/extensions.dart +++ b/packages/go_router/lib/src/misc/extensions.dart @@ -4,6 +4,7 @@ import 'package:flutter/widgets.dart'; +import '../match.dart'; import '../router.dart'; /// Dart extension to add navigation function to a BuildContext object, e.g. @@ -24,6 +25,10 @@ extension GoRouterHelper on BuildContext { void go(String location, {Object? extra}) => GoRouter.of(this).go(location, extra: extra); + /// Restore the RouteMatchList + void restore(RouteMatchList matchList) => + GoRouter.of(this).restore(matchList); + /// Navigate to a named route. void goNamed( String name, { diff --git a/packages/go_router/lib/src/route.dart b/packages/go_router/lib/src/route.dart index 0183f91ea54..898ec5cb205 100644 --- a/packages/go_router/lib/src/route.dart +++ b/packages/go_router/lib/src/route.dart @@ -27,34 +27,20 @@ typedef GoRouterWidgetBuilder = Widget Function( GoRouterState state, ); -/// The widget builder for [ShellRoute]. +/// The widget builder for [ShellRouteBase]. typedef ShellRouteBuilder = Widget Function( BuildContext context, - GoRouterState state, + ShellRouteState state, Widget child, ); -/// The page builder for [ShellRoute]. +/// The page builder for [ShellRouteBase]. typedef ShellRoutePageBuilder = Page Function( BuildContext context, - GoRouterState state, + ShellRouteState state, Widget child, ); -/// The widget builder for [StatefulShellRoute]. -typedef StatefulShellRouteBuilder = Widget Function( - BuildContext context, - GoRouterState state, - StatefulNavigationShell navigationShell, -); - -/// The page builder for [StatefulShellRoute]. -typedef StatefulShellRoutePageBuilder = Page Function( - BuildContext context, - GoRouterState state, - StatefulNavigationShell navigationShell, -); - /// Signature for functions used to build Navigators typedef NavigatorBuilder = Widget Function( List? observers, String? restorationScopeId); @@ -459,13 +445,18 @@ class GoRoute extends RouteBase { /// as [ShellRoute] and [StatefulShellRoute]. abstract class ShellRouteBase extends RouteBase { /// Constructs a [ShellRouteBase]. - const ShellRouteBase._({ + const ShellRouteBase({ super.redirect, + this.name, required super.routes, required super.parentNavigatorKey, }) : super._(); - static void _debugCheckSubRouteParentNavigatorKeys( + /// The name of the shell route. + final String? name; + + /// Helper method to validate the configuration of nested navigators. + static void debugCheckSubRouteParentNavigatorKeys( List subRoutes, GlobalKey navigatorKey) { for (final RouteBase route in subRoutes) { assert( @@ -475,7 +466,7 @@ abstract class ShellRouteBase extends RouteBase { if (route is GoRoute && route.redirectOnly) { // This route does not produce a page, need to check its sub-routes // instead. - _debugCheckSubRouteParentNavigatorKeys(route.routes, navigatorKey); + debugCheckSubRouteParentNavigatorKeys(route.routes, navigatorKey); } } } @@ -484,48 +475,56 @@ abstract class ShellRouteBase extends RouteBase { /// /// Returns null if this shell route does not build a Widget, but instead uses /// a Page to represent itself (see [buildPage]). - Widget? buildWidget(BuildContext context, GoRouterState state, - ShellRouteContext shellRouteContext); + Widget? buildWidget(BuildContext context, ShellRouteState state, + NavigatorBuilder navigatorBuilder); /// Attempts to build the Page representing this shell route. /// /// Returns null if this shell route does not build a Page, but instead uses /// a Widget to represent itself (see [buildWidget]). - Page? buildPage(BuildContext context, GoRouterState state, - ShellRouteContext shellRouteContext); + Page? buildPage(BuildContext context, ShellRouteState state, + NavigatorBuilder navigatorBuilder); + + /// Returns the keys for all the [Navigator]s managed by this shell route. + List> get nestedNavigatorKeys; /// Returns the key for the [Navigator] that is to be used for the specified /// immediate sub-route of this shell route. GlobalKey navigatorKeyForSubRoute(RouteBase subRoute); -} - -/// Context object used when building the shell and Navigator for a shell route. -class ShellRouteContext { - /// Constructs a [ShellRouteContext]. - ShellRouteContext({ - required this.route, - required this.routerState, - required this.navigatorKey, - required this.routeMatchList, - required this.navigatorBuilder, - }); - - /// The associated shell route. - final ShellRouteBase route; - - /// The current route state associated with [route]. - final GoRouterState routerState; - /// The [Navigator] key to be used for the nested navigation associated with - /// [route]. - final GlobalKey navigatorKey; - - /// The route match list representing the current location within the - /// associated shell route. - final RouteMatchList routeMatchList; + /// Returns the index of the [Navigator] key within the list of navigators + /// managed by this shell route, + /// + /// If this shell route does not support multiple navigators, this method + /// will return 0 if the navigator key matches the navigator key of this + /// route. If no matching navigator key is found, -1 will be returned. + int indexOfNavigatorKey(GlobalKey navigatorKey) => + nestedNavigatorKeys.indexOf(navigatorKey); + + /// Returns the location ([RouteMatchList]) associated with the current state + /// of the [Navigator] at the specified index, if any. + /// + /// Returns [RouteMatchList.empty] if a location cannot be determined (e.g. + /// if this is not supported by this shell route implementation). + /// + /// This implementation always [RouteMatchList.empty]. Custom shell route + /// implementations that support multiple nested navigators should override + /// this method to return current location of the Navigator at the specified + /// index. + RouteMatchList locationOfNavigator( + ShellRouteState state, int navigatorIndex) => + RouteMatchList.empty; + + /// Resets the location of the [Navigator] at the specified index to its + /// initial location, if supported. + /// + /// This implementation does nothing. Custom shell route implementations that + /// support multiple nested navigators should override this method to reset + /// the location of the Navigator at the specified index. + void resetLocationOfNavigator(ShellRouteState state, int navigatorIndex) {} - /// Function used to build the [Navigator] for the current route. - final NavigatorBuilder navigatorBuilder; + /// Debugging method to validate the configuration of nested navigators. + void debugValidateNestedNavigators(RouteConfiguration configuration); } /// A route that displays a UI shell around the matching child route. @@ -635,10 +634,9 @@ class ShellRoute extends ShellRouteBase { GlobalKey? navigatorKey, this.restorationScopeId, }) : assert(routes.isNotEmpty), - navigatorKey = navigatorKey ?? GlobalKey(), - super._() { + navigatorKey = navigatorKey ?? GlobalKey() { assert(() { - ShellRouteBase._debugCheckSubRouteParentNavigatorKeys( + ShellRouteBase.debugCheckSubRouteParentNavigatorKeys( routes, this.navigatorKey); return true; }()); @@ -661,22 +659,20 @@ class ShellRoute extends ShellRouteBase { final ShellRoutePageBuilder? pageBuilder; @override - Widget? buildWidget(BuildContext context, GoRouterState state, - ShellRouteContext shellRouteContext) { + Widget? buildWidget(BuildContext context, ShellRouteState state, + NavigatorBuilder navigatorBuilder) { if (builder != null) { - final Widget navigator = - shellRouteContext.navigatorBuilder(observers, restorationScopeId); + final Widget navigator = navigatorBuilder(observers, restorationScopeId); return builder!(context, state, navigator); } return null; } @override - Page? buildPage(BuildContext context, GoRouterState state, - ShellRouteContext shellRouteContext) { + Page? buildPage(BuildContext context, ShellRouteState state, + NavigatorBuilder navigatorBuilder) { if (pageBuilder != null) { - final Widget navigator = - shellRouteContext.navigatorBuilder(observers, restorationScopeId); + final Widget navigator = navigatorBuilder(observers, restorationScopeId); return pageBuilder!(context, state, navigator); } return null; @@ -697,6 +693,10 @@ class ShellRoute extends ShellRouteBase { /// its history. final String? restorationScopeId; + @override + List> get nestedNavigatorKeys => + >[navigatorKey]; + @override GlobalKey navigatorKeyForSubRoute(RouteBase subRoute) { assert(routes.contains(subRoute)); @@ -709,8 +709,13 @@ class ShellRoute extends ShellRouteBase { properties.add(DiagnosticsProperty>( 'navigatorKey', navigatorKey)); } + + @override + void debugValidateNestedNavigators(RouteConfiguration configuration) {} } +// TODO(tolo): Consider moving StatefulShellRoute to a separate file. + /// A route that displays a UI shell with separate [Navigator]s for its /// sub-routes. /// @@ -730,11 +735,11 @@ class ShellRoute extends ShellRouteBase { /// /// Like [ShellRoute], either a [builder] or a [pageBuilder] must be provided /// when creating a StatefulShellRoute. However, these builders differ slightly -/// in that they accept a [StatefulNavigationShell] parameter instead of a +/// in that they accept a [_StatefulNavigationShell] parameter instead of a /// child Widget. The StatefulNavigationShell can be used to access information /// about the state of the route, as well as to switch the active branch (i.e. /// restoring the navigation stack of another branch). The latter is -/// accomplished by using the method [StatefulNavigationShell.goBranch], for +/// accomplished by using the method [_StatefulNavigationShell.goBranch], for /// example: /// /// ``` @@ -788,21 +793,26 @@ class StatefulShellRoute extends ShellRouteBase { /// implementing the container for the branch Navigators is provided by /// [navigatorContainerBuilder]. StatefulShellRoute({ - required this.branches, - super.redirect, - this.builder, - this.pageBuilder, - required this.navigatorContainerBuilder, - super.parentNavigatorKey, - this.restorationScopeId, - }) : assert(branches.isNotEmpty), - 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)), - assert(_debugValidateRestorationScopeIds(restorationScopeId, branches)), - super._(routes: _routes(branches)); + String? name, + required List branches, + GoRouterRedirect? redirect, + ShellRouteBuilder? builder, + ShellRoutePageBuilder? pageBuilder, + required ShellNavigationContainerBuilder navigatorContainerBuilder, + GlobalKey? parentNavigatorKey, + String? restorationScopeId, + GlobalKey? key, + }) : this._( + name: name, + branches: branches, + redirect: redirect, + builder: builder, + pageBuilder: pageBuilder, + parentNavigatorKey: parentNavigatorKey, + restorationScopeId: restorationScopeId, + navigatorContainerBuilder: navigatorContainerBuilder, + key: key ?? GlobalKey(), + ); /// Constructs a StatefulShellRoute that uses an [IndexedStack] for its /// nested [Navigator]s. @@ -815,13 +825,16 @@ class StatefulShellRoute extends ShellRouteBase { /// See [Stateful Nested Navigation](https://github.com/flutter/packages/blob/main/packages/go_router/example/lib/stacked_shell_route.dart) /// for a complete runnable example using StatefulShellRoute.indexedStack. StatefulShellRoute.indexedStack({ + String? name, required List branches, GoRouterRedirect? redirect, - StatefulShellRouteBuilder? builder, + ShellRouteBuilder? builder, + ShellRoutePageBuilder? pageBuilder, GlobalKey? parentNavigatorKey, - StatefulShellRoutePageBuilder? pageBuilder, String? restorationScopeId, - }) : this( + GlobalKey>? key, + }) : this._( + name: name, branches: branches, redirect: redirect, builder: builder, @@ -829,41 +842,57 @@ class StatefulShellRoute extends ShellRouteBase { parentNavigatorKey: parentNavigatorKey, restorationScopeId: restorationScopeId, navigatorContainerBuilder: _indexedStackContainerBuilder, + key: key ?? GlobalKey(), ); + StatefulShellRoute._({ + super.name, + required this.branches, + super.redirect, + ShellRouteBuilder? builder, + this.pageBuilder, + required this.navigatorContainerBuilder, + super.parentNavigatorKey, + this.restorationScopeId, + required GlobalKey key, + }) : _shellStateKey = key, + assert(branches.isNotEmpty), + assert(_debugUniqueNavigatorKeys(branches).length == branches.length, + 'Navigator keys must be unique'), + assert(_debugValidateParentNavigatorKeys(branches)), + assert(_debugValidateRestorationScopeIds(restorationScopeId, branches)), + builder = builder ?? (pageBuilder == null ? _defaultBuilder : null), + super(routes: _routes(branches)); + /// Restoration ID to save and restore the state of the navigator, including /// its history. final String? restorationScopeId; /// The widget builder for a stateful shell route. /// - /// Similar to [GoRoute.builder], but with an additional - /// [StatefulNavigationShell] parameter. StatefulNavigationShell is a Widget - /// responsible for managing the nested navigation for the + /// Similar to [GoRoute.builder], but with an additional child parameter. This + /// child parameter is the Widget managing the nested navigation for the /// matching sub-routes. Typically, a shell route builds its shell around this - /// Widget. StatefulNavigationShell can also be used to access information - /// about which branch is active, and also to navigate to a different branch - /// (using [StatefulNavigationShell.goBranch]). + /// Widget. /// - /// Custom implementations may choose to ignore the child parameter passed to - /// the builder function, and instead use [StatefulNavigationShell] to - /// create a custom container for the branch Navigators. - final StatefulShellRouteBuilder? builder; + /// The state parameter of this function ([ShellRouteState]) can also be used + /// to access information about which branch is active (see + /// [ShellRouteState.navigatorIndex]), and also to navigate to a different + /// branch (using [ShellRouteState.restoreNavigator]). + final ShellRouteBuilder? builder; /// The page builder for a stateful shell route. /// - /// Similar to [GoRoute.pageBuilder], but with an additional - /// [StatefulNavigationShell] parameter. StatefulNavigationShell is a Widget - /// responsible for managing the nested navigation for the + /// Similar to [GoRoute.pageBuilder], but with an additional child parameter. + /// This child parameter is the Widget managing the nested navigation for the /// matching sub-routes. Typically, a shell route builds its shell around this - /// Widget. StatefulNavigationShell can also be used to access information - /// about which branch is active, and also to navigate to a different branch - /// (using [StatefulNavigationShell.goBranch]). + /// Widget. /// - /// Custom implementations may choose to ignore the child parameter passed to - /// the builder function, and instead use [StatefulNavigationShell] to - /// create a custom container for the branch Navigators. - final StatefulShellRoutePageBuilder? pageBuilder; + /// The state parameter of this function ([ShellRouteState]) can also be used + /// to access information about which branch is active (see + /// [ShellRouteState.navigatorIndex]), and also to navigate to a different + /// branch (using [ShellRouteState.restoreNavigator]). + final ShellRoutePageBuilder? pageBuilder; /// The builder for the branch Navigator container. /// @@ -884,28 +913,35 @@ class StatefulShellRoute extends ShellRouteBase { /// [StatefulShellBranch.navigatorKey]. final List branches; - final GlobalKey _shellStateKey = - GlobalKey(); + final GlobalKey _shellStateKey; + + _StatefulNavigationShellState? get _shellState => + _shellStateKey.currentState as _StatefulNavigationShellState?; @override - Widget? buildWidget(BuildContext context, GoRouterState state, - ShellRouteContext shellRouteContext) { + Widget? buildWidget(BuildContext context, ShellRouteState state, + NavigatorBuilder navigatorBuilder) { if (builder != null) { - return builder!(context, state, _createShell(context, shellRouteContext)); + return builder!( + context, state, _createShell(context, state, navigatorBuilder)); } return null; } @override - Page? buildPage(BuildContext context, GoRouterState state, - ShellRouteContext shellRouteContext) { + Page? buildPage(BuildContext context, ShellRouteState state, + NavigatorBuilder navigatorBuilder) { if (pageBuilder != null) { return pageBuilder!( - context, state, _createShell(context, shellRouteContext)); + context, state, _createShell(context, state, navigatorBuilder)); } return null; } + @override + List> get nestedNavigatorKeys => + branches.map((StatefulShellBranch b) => b.navigatorKey).toList(); + @override GlobalKey navigatorKeyForSubRoute(RouteBase subRoute) { final StatefulShellBranch? branch = branches.firstWhereOrNull( @@ -914,20 +950,55 @@ class StatefulShellRoute extends ShellRouteBase { return branch!.navigatorKey; } + @override + RouteMatchList locationOfNavigator(GoRouterState state, int navigatorIndex) { + final RouteMatchList? matchList = + _shellState?._matchListForBranch(navigatorIndex); + if (matchList != null && matchList.isNotEmpty) { + return matchList; + } else { + final String initialLocation = + branches[navigatorIndex]._effectiveInitialBranchLocation(state); + return state.findMatch(Uri.parse(initialLocation)); + } + } + + @override + void resetLocationOfNavigator(ShellRouteState state, int navigatorIndex) { + _shellState?._resetMatchListForBranch(navigatorIndex); + } + + /// Finds the [ShellRouteState] for the given [GlobalKey], associated with a + /// [StatefulShellRoute]. + static ShellRouteState? shellRouteStateForKey( + GlobalKey key) => + (key.currentState as _StatefulNavigationShellState?)?._routeState; + + /// Get the initial location for the branch at the given index. + /// + /// See also [StatefulShellBranch.initialLocation]). + String initialBranchLocation(ShellRouteState state, int branchIndex) => + branches[branchIndex]._effectiveInitialBranchLocation(state); + Iterable> get _navigatorKeys => branches.map((StatefulShellBranch b) => b.navigatorKey); - StatefulNavigationShell _createShell( - BuildContext context, ShellRouteContext shellRouteContext) => - StatefulNavigationShell( - shellRouteContext: shellRouteContext, + _StatefulNavigationShell _createShell(BuildContext context, + ShellRouteState state, NavigatorBuilder navigatorBuilder) => + _StatefulNavigationShell._( + routeState: state, + navigatorBuilder: navigatorBuilder, router: GoRouter.of(context), containerBuilder: navigatorContainerBuilder); - static Widget _indexedStackContainerBuilder(BuildContext context, - StatefulNavigationShell navigationShell, List children) { + static Widget _defaultBuilder( + BuildContext context, ShellRouteState state, Widget child) => + child; + + static Widget _indexedStackContainerBuilder( + BuildContext context, ShellRouteState state, List children) { return _IndexedStackedRouteBranchContainer( - currentIndex: navigationShell.currentIndex, children: children); + currentIndex: state.navigatorIndex, children: children); } static List _routes(List branches) => @@ -966,6 +1037,46 @@ class StatefulShellRoute extends ShellRouteBase { return true; } + @override + void debugValidateNestedNavigators(RouteConfiguration configuration) { + for (final StatefulShellBranch branch in branches) { + if (branch.initialLocation == null) { + // Recursively search for the first GoRoute descendant. Will + // throw assertion error if not found. + final GoRoute? route = branch.defaultRoute; + final String? initialLocation = + route != null ? configuration.locationForRoute(route) : null; + assert( + initialLocation != null, + 'The default location of a StatefulShellBranch must be ' + 'derivable from GoRoute descendant'); + assert( + route!.pathParameters.isEmpty, + 'The default location of a StatefulShellBranch cannot be ' + 'a parameterized route'); + } else { + final RouteMatchList matchList = + configuration.findMatch(Uri.parse(branch.initialLocation!)); + assert( + !matchList.isError, + 'initialLocation (${matchList.uri}) of StatefulShellBranch must ' + 'be a valid location'); + final List matchRoutes = matchList.routes; + final int shellIndex = matchRoutes.indexOf(this); + bool matchFound = false; + if (shellIndex >= 0 && (shellIndex + 1) < matchRoutes.length) { + final RouteBase branchRoot = matchRoutes[shellIndex + 1]; + matchFound = branch.routes.contains(branchRoot); + } + assert( + matchFound, + 'The initialLocation (${branch.initialLocation}) of ' + 'StatefulShellBranch must match a descendant route of the ' + 'branch'); + } + } + } + @override void debugFillProperties(DiagnosticPropertiesBuilder properties) { super.debugFillProperties(properties); @@ -981,7 +1092,7 @@ class StatefulShellRoute extends ShellRouteBase { /// sub-routes ([routes]), however sometimes it may be convenient to also /// provide a [initialLocation]. The value of this parameter is used when /// loading the branch for the first time (for instance when switching branch -/// using the goBranch method in [StatefulNavigationShell]). +/// using the goBranch method in [_StatefulNavigationShell]). /// /// A separate [Navigator] will be built for each StatefulShellBranch in a /// [StatefulShellRoute], and the routes of this branch will be placed onto that @@ -1000,7 +1111,7 @@ class StatefulShellBranch { this.observers, }) : navigatorKey = navigatorKey ?? GlobalKey() { assert(() { - ShellRouteBase._debugCheckSubRouteParentNavigatorKeys( + ShellRouteBase.debugCheckSubRouteParentNavigatorKeys( routes, this.navigatorKey); return true; }()); @@ -1040,147 +1151,68 @@ class StatefulShellBranch { /// an [initialLocation] has not been provided. GoRoute? get defaultRoute => RouteBase.routesRecursively(routes).whereType().firstOrNull; + + String _effectiveInitialBranchLocation(GoRouterState state) { + if (initialLocation != null) { + return initialLocation!; + } else { + /// Recursively traverses the routes of the provided StatefulShellRoute to + /// find the first GoRoute, from which a full path will be derived. + final GoRoute route = defaultRoute!; + final List parameters = []; + patternToRegExp(route.path, parameters); + assert(parameters.isEmpty); + final String fullPath = state.locationForRoute(route)!; + return patternToPath(fullPath, state.pathParameters); + } + } } /// Builder for a custom container for the branch Navigators of a /// [StatefulShellRoute]. -typedef ShellNavigationContainerBuilder = Widget Function(BuildContext context, - StatefulNavigationShell navigationShell, List children); +typedef ShellNavigationContainerBuilder = Widget Function( + BuildContext context, ShellRouteState shellState, List children); /// Widget for managing the state of a [StatefulShellRoute]. /// /// Normally, this widget is not used directly, but is instead created -/// internally by StatefulShellRoute. However, if a custom container for the -/// branch Navigators is required, StatefulNavigationShell can be used in -/// the builder or pageBuilder methods of StatefulShellRoute to facilitate this. -/// The container is created using the provided [ShellNavigationContainerBuilder], -/// where the List of Widgets represent the Navigators for each branch. -/// -/// Example: -/// ``` -/// builder: (BuildContext context, GoRouterState state, -/// StatefulNavigationShell navigationShell) { -/// return StatefulNavigationShell( -/// shellRouteState: state, -/// containerBuilder: (_, __, List children) => MyCustomShell(shellState: state, children: children), -/// ); -/// } -/// ``` -class StatefulNavigationShell extends StatefulWidget { - /// Constructs an [StatefulNavigationShell]. - StatefulNavigationShell({ - required this.shellRouteContext, +/// internally by [StatefulShellRoute]. +class _StatefulNavigationShell extends StatefulWidget { + /// Constructs an [_StatefulNavigationShell]. + _StatefulNavigationShell._({ + required ShellRouteState routeState, + required NavigatorBuilder navigatorBuilder, required GoRouter router, - required this.containerBuilder, - }) : assert(shellRouteContext.route is StatefulShellRoute), + required ShellNavigationContainerBuilder containerBuilder, + }) : _routeState = routeState, + _navigatorBuilder = navigatorBuilder, + _containerBuilder = containerBuilder, + assert(routeState.shellRoute is StatefulShellRoute), _router = router, - currentIndex = _indexOfBranchNavigatorKey( - shellRouteContext.route as StatefulShellRoute, - shellRouteContext.navigatorKey), super( - key: - (shellRouteContext.route as StatefulShellRoute)._shellStateKey); + key: (routeState.shellRoute as StatefulShellRoute)._shellStateKey); - /// The ShellRouteContext responsible for building the Navigator for the - /// current [StatefulShellBranch]. - final ShellRouteContext shellRouteContext; + final ShellRouteState _routeState; + final NavigatorBuilder _navigatorBuilder; final GoRouter _router; /// The builder for a custom container for shell route Navigators. - final ShellNavigationContainerBuilder containerBuilder; - - /// The index of the currently active [StatefulShellBranch]. - /// - /// Corresponds to the index in the branches field of [StatefulShellRoute]. - final int currentIndex; - - /// The associated [StatefulShellRoute]. - StatefulShellRoute get route => shellRouteContext.route as StatefulShellRoute; - - /// Navigate to the last location of the [StatefulShellBranch] at the provided - /// index in the associated [StatefulShellBranch]. - /// - /// This method will switch the currently active branch [Navigator] for the - /// [StatefulShellRoute]. If the branch has not been visited before, or if - /// initialLocation is true, this method will navigate to initial location of - /// the branch (see [StatefulShellBranch.initialLocation]). - // TODO(chunhtai): figure out a way to avoid putting navigation API in widget - // class. - void goBranch(int index, {bool initialLocation = false}) { - final StatefulShellRoute route = - shellRouteContext.route as StatefulShellRoute; - final StatefulNavigationShellState? shellState = - route._shellStateKey.currentState; - if (shellState != null) { - shellState.goBranch(index, initialLocation: initialLocation); - } else { - _router.go(_effectiveInitialBranchLocation(index)); - } - } - - /// Gets the effective initial location for the branch at the provided index - /// in the associated [StatefulShellRoute]. - /// - /// The effective initial location is either the - /// [StackedShellBranch.initialLocation], if specified, or the location of the - /// [StackedShellBranch.defaultRoute]. - String _effectiveInitialBranchLocation(int index) { - final StatefulShellRoute route = - shellRouteContext.route as StatefulShellRoute; - final StatefulShellBranch branch = route.branches[index]; - final String? initialLocation = branch.initialLocation; - if (initialLocation != null) { - return initialLocation; - } else { - /// Recursively traverses the routes of the provided StackedShellBranch to - /// find the first GoRoute, from which a full path will be derived. - final GoRoute route = branch.defaultRoute!; - final List parameters = []; - patternToRegExp(route.path, parameters); - assert(parameters.isEmpty); - final String fullPath = _router.configuration.locationForRoute(route)!; - return patternToPath( - fullPath, shellRouteContext.routerState.pathParameters); - } - } + final ShellNavigationContainerBuilder _containerBuilder; @override - State createState() => StatefulNavigationShellState(); - - /// Gets the state for the nearest stateful shell route in the Widget tree. - static StatefulNavigationShellState of(BuildContext context) { - final StatefulNavigationShellState? shellState = - context.findAncestorStateOfType(); - assert(shellState != null); - return shellState!; - } - - /// Gets the state for the nearest stateful shell route in the Widget tree. - /// - /// Returns null if no stateful shell route is found. - static StatefulNavigationShellState? maybeOf(BuildContext context) { - final StatefulNavigationShellState? shellState = - context.findAncestorStateOfType(); - return shellState; - } - - static int _indexOfBranchNavigatorKey( - StatefulShellRoute route, GlobalKey navigatorKey) { - final int index = route.branches.indexWhere( - (StatefulShellBranch branch) => branch.navigatorKey == navigatorKey); - assert(index >= 0); - return index; - } + State createState() => _StatefulNavigationShellState(); } -/// State for StatefulNavigationShell. -class StatefulNavigationShellState extends State +/// State for [_StatefulNavigationShell], and consequently [StatefulShellRoute]. +class _StatefulNavigationShellState extends State<_StatefulNavigationShell> with RestorationMixin { final Map _branchNavigators = {}; + ShellRouteState get _routeState => widget._routeState; + /// The associated [StatefulShellRoute]. - StatefulShellRoute get route => widget.route; + StatefulShellRoute get _route => _routeState.shellRoute as StatefulShellRoute; GoRouter get _router => widget._router; @@ -1188,7 +1220,7 @@ class StatefulNavigationShellState extends State {}; @override - String? get restorationId => route.restorationScopeId; + String? get restorationId => _route.restorationScopeId; /// Generates a derived restoration ID for the branch location property, /// falling back to the identity hash code of the branch to ensure an ID is @@ -1212,8 +1244,16 @@ class StatefulNavigationShellState extends State }); } - RouteMatchList? _matchListForBranch(int index) => - _branchLocations[route.branches[index]]?.value; + RouteMatchList? _matchListForBranch(int index) { + assert(index >= 0 && index < _route.branches.length); + return _branchLocations[_route.branches[index]]?.value; + } + + void _resetMatchListForBranch(int index) { + final StatefulShellBranch branch = _route.branches[index]; + _branchNavigators.remove(branch.navigatorKey); + _branchLocation(branch, false).value = RouteMatchList.empty; + } /// Creates a new RouteMatchList that is scoped to the Navigators of the /// current shell route or it's descendants. This involves removing all the @@ -1227,7 +1267,7 @@ class StatefulNavigationShellState extends State final List result = []; for (final RouteMatchBase match in matches) { if (match is ShellRouteMatch) { - if (match.route == route) { + if (match.route == _route) { result.add(match); // Discard any other route match after current shell route. break; @@ -1241,10 +1281,10 @@ class StatefulNavigationShellState extends State } void _updateCurrentBranchStateFromWidget() { - final StatefulShellBranch branch = route.branches[widget.currentIndex]; - final ShellRouteContext shellRouteContext = widget.shellRouteContext; + final StatefulShellBranch branch = + _route.branches[_routeState.navigatorIndex]; final RouteMatchList currentBranchLocation = - _scopedMatchList(shellRouteContext.routeMatchList); + _scopedMatchList(_routeState.routeMatchList); final _RestorableRouteMatchList branchLocation = _branchLocation(branch, false); @@ -1257,31 +1297,8 @@ class StatefulNavigationShellState extends State final bool locationChanged = previousBranchLocation != currentBranchLocation; if (locationChanged || !hasExistingNavigator) { - _branchNavigators[branch.navigatorKey] = shellRouteContext - .navigatorBuilder(branch.observers, branch.restorationScopeId); - } - } - - /// The index of the currently active [StatefulShellBranch]. - /// - /// Corresponds to the index in the branches field of [StatefulShellRoute]. - int get currentIndex => widget.currentIndex; - - /// Navigate to the last location of the [StatefulShellBranch] at the provided - /// index in the associated [StatefulShellBranch]. - /// - /// This method will switch the currently active branch [Navigator] for the - /// [StatefulShellRoute]. If the branch has not been visited before, or if - /// initialLocation is true, this method will navigate to initial location of - /// the branch (see [StatefulShellBranch.initialLocation]). - void goBranch(int index, {bool initialLocation = false}) { - assert(index >= 0 && index < route.branches.length); - final RouteMatchList? matchList = - initialLocation ? null : _matchListForBranch(index); - if (matchList != null && matchList.isNotEmpty) { - _router.restore(matchList); - } else { - _router.go(widget._effectiveInitialBranchLocation(index)); + _branchNavigators[branch.navigatorKey] = + widget._navigatorBuilder(branch.observers, branch.restorationScopeId); } } @@ -1294,25 +1311,25 @@ class StatefulNavigationShellState extends State @override void dispose() { super.dispose(); - for (final StatefulShellBranch branch in route.branches) { + for (final StatefulShellBranch branch in _route.branches) { _branchLocations[branch]?.dispose(); } } @override void restoreState(RestorationBucket? oldBucket, bool initialRestore) { - route.branches.forEach(_branchLocation); + _route.branches.forEach(_branchLocation); } @override - void didUpdateWidget(covariant StatefulNavigationShell oldWidget) { + void didUpdateWidget(covariant _StatefulNavigationShell oldWidget) { super.didUpdateWidget(oldWidget); _updateCurrentBranchStateFromWidget(); } @override Widget build(BuildContext context) { - final List children = route.branches + final List children = _route.branches .map((StatefulShellBranch branch) => _BranchNavigatorProxy( key: ObjectKey(branch), branch: branch, @@ -1320,7 +1337,7 @@ class StatefulNavigationShellState extends State _branchNavigators[b.navigatorKey])) .toList(); - return widget.containerBuilder(context, widget, children); + return widget._containerBuilder(context, _routeState, children); } } diff --git a/packages/go_router/lib/src/route_data.dart b/packages/go_router/lib/src/route_data.dart index afe6159cecb..920d3894788 100644 --- a/packages/go_router/lib/src/route_data.dart +++ b/packages/go_router/lib/src/route_data.dart @@ -244,16 +244,16 @@ abstract class StatefulShellRouteData extends RouteData { /// [pageBuilder] is used to build the page Page pageBuilder( BuildContext context, - GoRouterState state, - StatefulNavigationShell navigationShell, + ShellRouteState state, + Widget navigationShell, ) => const NoOpPage(); /// [builder] is used to build the widget Widget builder( BuildContext context, - GoRouterState state, - StatefulNavigationShell navigationShell, + ShellRouteState state, + Widget navigationShell, ) => throw UnimplementedError( 'One of `builder` or `pageBuilder` must be implemented.', @@ -275,8 +275,8 @@ abstract class StatefulShellRouteData extends RouteData { Widget builder( BuildContext context, - GoRouterState state, - StatefulNavigationShell navigationShell, + ShellRouteState state, + Widget navigationShell, ) => factoryImpl(state).builder( context, @@ -286,8 +286,8 @@ abstract class StatefulShellRouteData extends RouteData { Page pageBuilder( BuildContext context, - GoRouterState state, - StatefulNavigationShell navigationShell, + ShellRouteState state, + Widget navigationShell, ) => factoryImpl(state).pageBuilder( context, diff --git a/packages/go_router/lib/src/state.dart b/packages/go_router/lib/src/state.dart index 323321b6a8b..6edcd373a41 100644 --- a/packages/go_router/lib/src/state.dart +++ b/packages/go_router/lib/src/state.dart @@ -6,6 +6,7 @@ import 'package:flutter/widgets.dart'; import 'package:meta/meta.dart'; import 'configuration.dart'; +import 'match.dart'; import 'misc/errors.dart'; import 'route.dart'; @@ -162,6 +163,17 @@ class GoRouterState { pathParameters: pathParameters, queryParameters: queryParameters); } + /// Get the location for the provided route. + String? locationForRoute(RouteBase route) => + _configuration.locationForRoute(route); + + /// Finds the routes that matched the given URL. + RouteMatchList findMatch(Uri uri, {Object? extra}) => + _configuration.findMatch( + uri, + extra: extra, + ); + @override bool operator ==(Object other) { return other is GoRouterState && @@ -190,6 +202,82 @@ class GoRouterState { ); } +/// The route state of a [ShellRouteBase] (i.e. [ShellRoute] or +/// [StatefulShellRoute]) during routing. +class ShellRouteState extends GoRouterState { + /// Constructs a [ShellRouteState]. + const ShellRouteState( + super._configuration, { + required super.uri, + required super.matchedLocation, + super.name, + super.path, + required super.fullPath, + required super.pathParameters, + super.extra, + super.error, + required super.pageKey, + super.topRoute, + required this.navigatorKey, + required this.shellRoute, + required this.routeMatchList, + }); + + /// The [Navigator] key to be used for the nested navigation associated with + /// [shellRoute]. + final GlobalKey navigatorKey; + + /// The associated [ShellRouteBase] for this state. + final ShellRouteBase shellRoute; + + /// The route match list representing the current location within the + /// associated shell route. + final RouteMatchList routeMatchList; + + /// Get the index of the [Navigator] or [StatefulShellBranch] in the + /// associated shell route. + /// + /// See also: [ShellRouteBase.indexStatefulShellBranchOfNavigatorKey]. + int get navigatorIndex => shellRoute.indexOfNavigatorKey(navigatorKey); + + /// Get the location ([RouteMatchList]) associated with the current state + /// of the [Navigator] at the specified index, if any. + /// + /// See also: [ShellRouteBase.locationOfNavigator]. + RouteMatchList navigatorLocation(int navigatorIndex) => + shellRoute.locationOfNavigator(this, navigatorIndex); + + /// Resets the location of the [Navigator] at the specified index to its + /// initial location, if supported. + /// + /// See also: [ShellRouteBase.resetLocationOfNavigator]. + void resetNavigatorLocation(int navigatorIndex) => + shellRoute.resetLocationOfNavigator(this, navigatorIndex); + + /// Gets the [ShellRouteState] from context. + static ShellRouteState of(BuildContext context, {String? name}) { + while (true) { + final GoRouterStateRegistryScope? scope = context + .dependOnInheritedWidgetOfExactType(); + if (scope?.notifier?.parentShellRouteState == null) { + throw GoRouterState._noGoRouterStateError; + } + final ShellRouteState shellRouteState = + scope!.notifier!.parentShellRouteState!; + if (name == null || shellRouteState.name == name) { + return shellRouteState; + } + + final NavigatorState? state = + scope.notifier!.parentNavigatorKey?.currentState; + if (state == null) { + throw GoRouterState._noGoRouterStateError; + } + context = state.context; + } + } +} + /// An inherited widget to host a [GoRouterStateRegistry] for the subtree. /// /// Should not be used directly, consider using [GoRouterState.of] to access @@ -212,7 +300,10 @@ class GoRouterStateRegistryScope @internal class GoRouterStateRegistry extends ChangeNotifier { /// creates a [GoRouterStateRegistry]. - GoRouterStateRegistry(); + GoRouterStateRegistry(this.parentNavigatorKey, this.parentShellRouteState); + + final GlobalKey? parentNavigatorKey; + final ShellRouteState? parentShellRouteState; /// A [Map] that maps a [Page] to a [GoRouterState]. @visibleForTesting diff --git a/packages/go_router/test/builder_test.dart b/packages/go_router/test/builder_test.dart index 7b260d5250e..f8ff5168111 100644 --- a/packages/go_router/test/builder_test.dart +++ b/packages/go_router/test/builder_test.dart @@ -351,9 +351,9 @@ void main() { routes: [ StatefulShellRoute.indexedStack( restorationScopeId: 'shell', - builder: (BuildContext context, GoRouterState state, - StatefulNavigationShell navigationShell) => - _HomeScreen(child: navigationShell), + builder: + (BuildContext context, ShellRouteState state, Widget child) => + _HomeScreen(child: child), branches: [ StatefulShellBranch( navigatorKey: shellNavigatorKey, diff --git a/packages/go_router/test/configuration_test.dart b/packages/go_router/test/configuration_test.dart index 0929756599f..6a81d813536 100644 --- a/packages/go_router/test/configuration_test.dart +++ b/packages/go_router/test/configuration_test.dart @@ -112,7 +112,7 @@ void main() { ]), ], ), - ], builder: mockStackedShellBuilder), + ], builder: mockStatefulShellBuilder), ], redirectLimit: 10, topRedirect: (BuildContext context, GoRouterState state) { @@ -150,7 +150,7 @@ void main() { ]), ], ), - ], builder: mockStackedShellBuilder), + ], builder: mockStatefulShellBuilder), ], redirectLimit: 10, topRedirect: (BuildContext context, GoRouterState state) { @@ -193,7 +193,7 @@ void main() { parentNavigatorKey: someNavigatorKey), ], ), - ], builder: mockStackedShellBuilder), + ], builder: mockStatefulShellBuilder), ], redirectLimit: 10, topRedirect: (BuildContext context, GoRouterState state) { @@ -223,7 +223,7 @@ void main() { routes: [ StatefulShellRoute.indexedStack(branches: [ StatefulShellBranch(routes: shellRouteChildren) - ], builder: mockStackedShellBuilder), + ], builder: mockStatefulShellBuilder), ], redirectLimit: 10, topRedirect: (BuildContext context, GoRouterState state) { @@ -264,7 +264,7 @@ void main() { StatefulShellBranch( routes: [routeB], navigatorKey: sectionBNavigatorKey), - ], builder: mockStackedShellBuilder), + ], builder: mockStatefulShellBuilder), ], redirectLimit: 10, topRedirect: (BuildContext context, GoRouterState state) { @@ -310,7 +310,7 @@ void main() { ), ], ), - ], builder: mockStackedShellBuilder), + ], builder: mockStatefulShellBuilder), ], redirectLimit: 10, topRedirect: (BuildContext context, GoRouterState state) { @@ -362,10 +362,10 @@ void main() { ], ), ], - builder: mockStackedShellBuilder), + builder: mockStatefulShellBuilder), ], ), - ], builder: mockStackedShellBuilder), + ], builder: mockStatefulShellBuilder), ], redirectLimit: 10, topRedirect: (BuildContext context, GoRouterState state) { @@ -445,7 +445,7 @@ void main() { ]), ], ), - ], builder: mockStackedShellBuilder), + ], builder: mockStatefulShellBuilder), ], ), StatefulShellBranch(routes: [ @@ -464,7 +464,7 @@ void main() { ], ), ]), - ], builder: mockStackedShellBuilder), + ], builder: mockStatefulShellBuilder), ], redirectLimit: 10, topRedirect: (BuildContext context, GoRouterState state) { @@ -484,7 +484,7 @@ void main() { navigatorKey: GlobalKey(debugLabel: 'root'), routes: [ StatefulShellRoute.indexedStack( - builder: mockStackedShellBuilder, + builder: mockStatefulShellBuilder, branches: [ branchA = StatefulShellBranch(routes: [ GoRoute( @@ -496,7 +496,7 @@ void main() { builder: _mockScreenBuilder, routes: [ StatefulShellRoute.indexedStack( - builder: mockStackedShellBuilder, + builder: mockStatefulShellBuilder, branches: [ branchY = StatefulShellBranch(routes: [ @@ -1101,6 +1101,6 @@ Widget _mockShellBuilder( BuildContext context, GoRouterState state, Widget child) => child; -Widget _mockIndexedStackShellBuilder(BuildContext context, GoRouterState state, - StatefulNavigationShell shell) => - shell; +Widget _mockIndexedStackShellBuilder( + BuildContext context, ShellRouteState state, Widget child) => + child; diff --git a/packages/go_router/test/delegate_test.dart b/packages/go_router/test/delegate_test.dart index 6513a1c9665..19f084e2769 100644 --- a/packages/go_router/test/delegate_test.dart +++ b/packages/go_router/test/delegate_test.dart @@ -66,7 +66,7 @@ Future createGoRouterWithStatefulShellRoute( builder: (_, __) => const DummyStatefulWidget()), ]), ]), - ], builder: mockStackedShellBuilder), + ], builder: mockStatefulShellBuilder), ], ); addTearDown(router.dispose); diff --git a/packages/go_router/test/go_route_test.dart b/packages/go_router/test/go_route_test.dart index 0ff437c0c1c..879fb7fcb37 100644 --- a/packages/go_router/test/go_route_test.dart +++ b/packages/go_router/test/go_route_test.dart @@ -119,11 +119,11 @@ void main() { routes: [ StatefulShellRoute.indexedStack( parentNavigatorKey: rootNavigatorKey, - builder: (_, __, StatefulNavigationShell navigationShell) { + builder: (_, __, Widget child) { return Column( children: [ const Text('Screen D'), - Expanded(child: navigationShell), + Expanded(child: child), ], ); }, diff --git a/packages/go_router/test/go_router_state_test.dart b/packages/go_router/test/go_router_state_test.dart index 4c588177eb2..c69471722be 100644 --- a/packages/go_router/test/go_router_state_test.dart +++ b/packages/go_router/test/go_router_state_test.dart @@ -251,8 +251,8 @@ void main() { parentNavigatorKey: rootNavigatorKey, builder: ( BuildContext context, - GoRouterState state, - StatefulNavigationShell navigationShell, + ShellRouteState state, + Widget child, ) { final String? routeName = GoRouterState.of(context).topRoute?.name; @@ -264,7 +264,7 @@ void main() { return Column( children: [ Text(title), - Expanded(child: navigationShell), + Expanded(child: child), ], ); }, diff --git a/packages/go_router/test/go_router_test.dart b/packages/go_router/test/go_router_test.dart index 10e6bb58d5b..ccb1cf45c5c 100644 --- a/packages/go_router/test/go_router_test.dart +++ b/packages/go_router/test/go_router_test.dart @@ -2590,9 +2590,8 @@ void main() { final List routes = [ StatefulShellRoute.indexedStack( redirect: (BuildContext context, GoRouterState state) => '/dummy', - builder: (BuildContext context, GoRouterState state, - StatefulNavigationShell navigationShell) { - return navigationShell; + builder: (BuildContext context, ShellRouteState state, Widget child) { + return child; }, branches: [ StatefulShellBranch( @@ -3110,13 +3109,12 @@ void main() { testWidgets('StatefulShellRoute supports nested routes with params', (WidgetTester tester) async { - StatefulNavigationShell? routeState; + ShellRouteState? routeState; final List routes = [ StatefulShellRoute.indexedStack( - builder: (BuildContext context, GoRouterState state, - StatefulNavigationShell navigationShell) { - routeState = navigationShell; - return navigationShell; + builder: (BuildContext context, ShellRouteState state, Widget child) { + routeState = state; + return child; }, branches: [ StatefulShellBranch( @@ -3178,12 +3176,12 @@ void main() { expect(matches.pathParameters['fid'], fid); expect(matches.pathParameters['pid'], pid); - routeState?.goBranch(0); + router.restore(routeState!.navigatorLocation(0)); await tester.pumpAndSettle(); expect(find.text('Screen A'), findsOneWidget); expect(find.byType(PersonScreen), findsNothing); - routeState?.goBranch(1); + router.restore(routeState!.navigatorLocation(1)); await tester.pumpAndSettle(); expect(find.text('Screen A'), findsNothing); expect(find.byType(PersonScreen), findsOneWidget); @@ -3194,14 +3192,13 @@ void main() { testWidgets('StatefulShellRoute preserve extra when switching branch', (WidgetTester tester) async { - StatefulNavigationShell? routeState; + ShellRouteState? routeState; Object? latestExtra; final List routes = [ StatefulShellRoute.indexedStack( - builder: (BuildContext context, GoRouterState state, - StatefulNavigationShell navigationShell) { - routeState = navigationShell; - return navigationShell; + builder: (BuildContext context, ShellRouteState state, Widget child) { + routeState = state; + return child; }, branches: [ StatefulShellBranch( @@ -3228,12 +3225,12 @@ void main() { ]; final Object expectedExtra = Object(); - await createRouter(routes, tester, + final GoRouter router = await createRouter(routes, tester, initialLocation: '/b', initialExtra: expectedExtra); expect(latestExtra, expectedExtra); - routeState!.goBranch(0); + router.restore(routeState!.navigatorLocation(0)); await tester.pumpAndSettle(); - routeState!.goBranch(1); + router.restore(routeState!.navigatorLocation(1)); await tester.pumpAndSettle(); expect(latestExtra, expectedExtra); }); @@ -3796,9 +3793,9 @@ void main() { final List routes = [ StatefulShellRoute.indexedStack( - builder: (BuildContext context, GoRouterState state, - StatefulNavigationShell navigationShell) => - navigationShell, + builder: + (BuildContext context, ShellRouteState state, Widget child) => + child, branches: [ StatefulShellBranch(routes: [ GoRoute( @@ -3841,9 +3838,9 @@ void main() { const Text('Root'), routes: [ StatefulShellRoute.indexedStack( - builder: (BuildContext context, GoRouterState state, - StatefulNavigationShell navigationShell) => - navigationShell, + builder: + (BuildContext context, ShellRouteState state, Widget child) => + child, branches: [ StatefulShellBranch(routes: [ GoRoute( @@ -3883,14 +3880,13 @@ void main() { GlobalKey(); final GlobalKey statefulWidgetKey = GlobalKey(); - StatefulNavigationShell? routeState; + ShellRouteState? routeState; final List routes = [ StatefulShellRoute.indexedStack( - builder: (BuildContext context, GoRouterState state, - StatefulNavigationShell navigationShell) { - routeState = navigationShell; - return navigationShell; + builder: (BuildContext context, ShellRouteState state, Widget child) { + routeState = state; + return child; }, branches: [ StatefulShellBranch( @@ -3933,7 +3929,7 @@ void main() { ), ]; - await createRouter(routes, tester, + final GoRouter router = await createRouter(routes, tester, initialLocation: '/a', navigatorKey: rootNavigatorKey); statefulWidgetKey.currentState?.increment(); expect(find.text('Screen A'), findsOneWidget); @@ -3941,21 +3937,21 @@ void main() { expect(find.text('Screen C'), findsNothing); expect(find.text('Screen D'), findsNothing); - routeState!.goBranch(1); + router.restore(routeState!.navigatorLocation(1)); await tester.pumpAndSettle(); expect(find.text('Screen A'), findsNothing); expect(find.text('Screen B'), findsOneWidget); expect(find.text('Screen C'), findsNothing); expect(find.text('Screen D'), findsNothing); - routeState!.goBranch(2); + router.restore(routeState!.navigatorLocation(2)); await tester.pumpAndSettle(); expect(find.text('Screen A'), findsNothing); expect(find.text('Screen B'), findsNothing); expect(find.text('Screen C'), findsOneWidget); expect(find.text('Screen D'), findsNothing); - routeState!.goBranch(3); + router.restore(routeState!.navigatorLocation(3)); await tester.pumpAndSettle(); expect(find.text('Screen A'), findsNothing); expect(find.text('Screen B'), findsNothing); @@ -3964,7 +3960,7 @@ void main() { expect(() { // Verify that navigation to unknown index fails - routeState!.goBranch(4); + router.restore(routeState!.navigatorLocation(4)); }, throwsA(isA())); }); @@ -3975,14 +3971,13 @@ void main() { GlobalKey(); final GlobalKey statefulWidgetKey = GlobalKey(); - StatefulNavigationShell? routeState; + ShellRouteState? routeState; final List routes = [ StatefulShellRoute.indexedStack( - builder: (BuildContext context, GoRouterState state, - StatefulNavigationShell navigationShell) { - routeState = navigationShell; - return navigationShell; + builder: (BuildContext context, ShellRouteState state, Widget child) { + routeState = state; + return child; }, branches: [ StatefulShellBranch(routes: [ @@ -4020,13 +4015,13 @@ void main() { expect(find.text('Screen A Detail'), findsOneWidget); expect(find.text('Screen B'), findsNothing); - routeState!.goBranch(1); + router.restore(routeState!.navigatorLocation(1)); await tester.pumpAndSettle(); expect(find.text('Screen A'), findsNothing); expect(find.text('Screen A Detail'), findsNothing); expect(find.text('Screen B'), findsOneWidget); - routeState!.goBranch(0); + router.restore(routeState!.navigatorLocation(0)); await tester.pumpAndSettle(); expect(statefulWidgetKey.currentState?.counter, equals(1)); @@ -4042,7 +4037,7 @@ void main() { testWidgets( 'Navigates to correct nested navigation tree in StatefulShellRoute ' 'and maintains path parameters', (WidgetTester tester) async { - StatefulNavigationShell? routeState; + ShellRouteState? routeState; final List routes = [ GoRoute( @@ -4050,10 +4045,10 @@ void main() { builder: (_, __) => const Placeholder(), routes: [ StatefulShellRoute.indexedStack( - builder: (BuildContext context, GoRouterState state, - StatefulNavigationShell navigationShell) { - routeState = navigationShell; - return navigationShell; + builder: (BuildContext context, ShellRouteState state, + Widget child) { + routeState = state; + return child; }, branches: [ StatefulShellBranch(routes: [ @@ -4075,10 +4070,11 @@ void main() { ]) ]; - await createRouter(routes, tester, initialLocation: '/123/a'); + final GoRouter router = + await createRouter(routes, tester, initialLocation: '/123/a'); expect(find.text('a id is 123'), findsOneWidget); - routeState!.goBranch(1); + router.restore(routeState!.navigatorLocation(1)); await tester.pumpAndSettle(); expect(find.text('b id is 123'), findsOneWidget); }); @@ -4089,23 +4085,22 @@ void main() { GlobalKey(); final GlobalKey statefulWidgetKey = GlobalKey(); - StatefulNavigationShell? routeState1; - StatefulNavigationShell? routeState2; + ShellRouteState? routeState1; + ShellRouteState? routeState2; final List routes = [ StatefulShellRoute.indexedStack( - builder: (BuildContext context, GoRouterState state, - StatefulNavigationShell navigationShell) { - routeState1 = navigationShell; - return navigationShell; + builder: (BuildContext context, ShellRouteState state, Widget child) { + routeState1 = state; + return child; }, branches: [ StatefulShellBranch(routes: [ StatefulShellRoute.indexedStack( - builder: (BuildContext context, GoRouterState state, - StatefulNavigationShell navigationShell) { - routeState2 = navigationShell; - return navigationShell; + builder: (BuildContext context, ShellRouteState state, + Widget child) { + routeState2 = state; + return child; }, branches: [ StatefulShellBranch(routes: [ @@ -4153,27 +4148,27 @@ void main() { ), ]; - await createRouter(routes, tester, + final GoRouter router = await createRouter(routes, tester, initialLocation: '/a/detailA', navigatorKey: rootNavigatorKey); statefulWidgetKey.currentState?.increment(); expect(find.text('Screen A Detail'), findsOneWidget); - routeState2!.goBranch(1); + router.restore(routeState2!.navigatorLocation(1)); await tester.pumpAndSettle(); expect(find.text('Screen B'), findsOneWidget); - routeState1!.goBranch(1); + router.restore(routeState1!.navigatorLocation(1)); await tester.pumpAndSettle(); expect(find.text('Screen D'), findsOneWidget); - routeState1!.goBranch(0); + router.restore(routeState1!.navigatorLocation(0)); await tester.pumpAndSettle(); expect(find.text('Screen B'), findsOneWidget); - routeState2!.goBranch(2); + router.restore(routeState2!.navigatorLocation(2)); await tester.pumpAndSettle(); expect(find.text('Screen C'), findsOneWidget); - routeState2!.goBranch(0); + router.restore(routeState2!.navigatorLocation(0)); await tester.pumpAndSettle(); expect(find.text('Screen A Detail'), findsOneWidget); expect(statefulWidgetKey.currentState?.counter, equals(1)); @@ -4188,14 +4183,13 @@ void main() { GlobalKey(); final GlobalKey sectionBNavigatorKey = GlobalKey(); - StatefulNavigationShell? routeState; + ShellRouteState? routeState; final List routes = [ StatefulShellRoute.indexedStack( - builder: (BuildContext context, GoRouterState state, - StatefulNavigationShell navigationShell) { - routeState = navigationShell; - return navigationShell; + builder: (BuildContext context, ShellRouteState state, Widget child) { + routeState = state; + return child; }, branches: [ StatefulShellBranch( @@ -4257,7 +4251,7 @@ void main() { expect(find.text('Screen B'), findsOneWidget); expect(find.text('Screen B Detail'), findsNothing); - routeState!.goBranch(0); + router.restore(routeState!.navigatorLocation(0)); await tester.pumpAndSettle(); expect(find.text('Screen A'), findsNothing); expect(find.text('Screen A Detail'), findsOneWidget); @@ -4273,14 +4267,13 @@ void main() { 'between branches in StatefulShellRoute', (WidgetTester tester) async { final GlobalKey rootNavigatorKey = GlobalKey(); - StatefulNavigationShell? routeState; + ShellRouteState? routeState; final List routes = [ StatefulShellRoute.indexedStack( - builder: (BuildContext context, GoRouterState state, - StatefulNavigationShell navigationShell) { - routeState = navigationShell; - return navigationShell; + builder: (BuildContext context, ShellRouteState state, Widget child) { + routeState = state; + return child; }, branches: [ StatefulShellBranch(routes: [ @@ -4310,12 +4303,12 @@ void main() { expect(find.text('Screen A'), findsNothing); expect(find.text('Screen B - X'), findsOneWidget); - routeState!.goBranch(0); + router.restore(routeState!.navigatorLocation(0)); await tester.pumpAndSettle(); expect(find.text('Screen A'), findsOneWidget); expect(find.text('Screen B - X'), findsNothing); - routeState!.goBranch(1); + router.restore(routeState!.navigatorLocation(1)); await tester.pumpAndSettle(); expect(find.text('Screen A'), findsNothing); expect(find.text('Screen B - X'), findsOneWidget); @@ -4327,7 +4320,7 @@ void main() { (WidgetTester tester) async { final GlobalKey rootNavigatorKey = GlobalKey(); - StatefulNavigationShell? routeState; + ShellRouteState? routeState; final List routes = [ GoRoute( @@ -4336,10 +4329,9 @@ void main() { Text('Common - ${state.extra}'), ), StatefulShellRoute.indexedStack( - builder: (BuildContext context, GoRouterState state, - StatefulNavigationShell navigationShell) { - routeState = navigationShell; - return navigationShell; + builder: (BuildContext context, ShellRouteState state, Widget child) { + routeState = state; + return child; }, branches: [ StatefulShellBranch(routes: [ @@ -4376,11 +4368,11 @@ void main() { expect(find.text('Screen B'), findsNothing); expect(find.text('Common - X'), findsOneWidget); - routeState!.goBranch(0); + router.restore(routeState!.navigatorLocation(0)); await tester.pumpAndSettle(); expect(find.text('Screen A'), findsOneWidget); - routeState!.goBranch(1); + router.restore(routeState!.navigatorLocation(1)); await tester.pumpAndSettle(); expect(find.text('Screen A'), findsNothing); expect(find.text('Screen B'), findsOneWidget); @@ -4391,14 +4383,13 @@ void main() { 'StatefulShellRoute', (WidgetTester tester) async { final GlobalKey rootNavigatorKey = GlobalKey(); - StatefulNavigationShell? routeState; + ShellRouteState? routeState; final List routes = [ StatefulShellRoute.indexedStack( - builder: (BuildContext context, GoRouterState state, - StatefulNavigationShell navigationShell) { - routeState = navigationShell; - return navigationShell; + builder: (BuildContext context, ShellRouteState state, Widget child) { + routeState = state; + return child; }, branches: [ StatefulShellBranch(routes: [ @@ -4448,7 +4439,7 @@ void main() { ]; String redirectDestinationBranchB = '/b/details1'; - await createRouter( + final GoRouter router = await createRouter( routes, tester, initialLocation: '/a', @@ -4463,19 +4454,19 @@ void main() { expect(find.text('Screen A'), findsOneWidget); expect(find.text('Screen B Detail'), findsNothing); - routeState!.goBranch(1); + router.restore(routeState!.navigatorLocation(1)); await tester.pumpAndSettle(); expect(find.text('Screen A'), findsNothing); expect(find.text('Screen B Detail1'), findsOneWidget); - routeState!.goBranch(2); + router.restore(routeState!.navigatorLocation(2)); await tester.pumpAndSettle(); expect(find.text('Screen A'), findsNothing); expect(find.text('Screen B Detail1'), findsNothing); expect(find.text('Screen C2'), findsOneWidget); redirectDestinationBranchB = '/b/details2'; - routeState!.goBranch(1); + router.restore(routeState!.navigatorLocation(1)); await tester.pumpAndSettle(); expect(find.text('Screen A'), findsNothing); expect(find.text('Screen B Detail2'), findsOneWidget); @@ -4489,15 +4480,14 @@ void main() { GlobalKey(); final GlobalKey nestedNavigatorKey = GlobalKey(); - StatefulNavigationShell? routeState; + ShellRouteState? routeState; final List routes = [ // First level shell StatefulShellRoute.indexedStack( - builder: (BuildContext context, GoRouterState state, - StatefulNavigationShell navigationShell) { - routeState = navigationShell; - return navigationShell; + builder: (BuildContext context, ShellRouteState state, Widget child) { + routeState = state; + return child; }, branches: [ StatefulShellBranch(routes: [ @@ -4510,9 +4500,9 @@ void main() { StatefulShellBranch(routes: [ // Second level / nested shell StatefulShellRoute.indexedStack( - builder: (BuildContext context, GoRouterState state, - StatefulNavigationShell navigationShell) => - navigationShell, + builder: (BuildContext context, ShellRouteState state, + Widget child) => + child, branches: [ StatefulShellBranch(routes: [ GoRoute( @@ -4559,7 +4549,7 @@ void main() { initialLocation: '/a', navigatorKey: rootNavigatorKey); expect(find.text('Screen A'), findsOneWidget); - routeState!.goBranch(1); + router.restore(routeState!.navigatorLocation(1)); await tester.pumpAndSettle(); expect(find.text('Screen B1'), findsOneWidget); @@ -4585,7 +4575,7 @@ void main() { // Switch to second branch, which should only contain 'Nested Modal' // (in the nested shell) - routeState!.goBranch(1); + router.restore(routeState!.navigatorLocation(1)); await tester.pumpAndSettle(); expect(find.text('Screen A'), findsNothing); expect(find.text('Screen B1'), findsNothing); @@ -4724,7 +4714,7 @@ void main() { initialLocation: '/a', routes: [ StatefulShellRoute.indexedStack( - builder: mockStackedShellBuilder, + builder: mockStatefulShellBuilder, branches: [ StatefulShellBranch(routes: [ GoRoute( @@ -5145,16 +5135,16 @@ void main() { GlobalKey(); final GlobalKey statefulWidgetKeyC = GlobalKey(); - StatefulNavigationShell? routeState; + ShellRouteState? routeState; final List routes = [ StatefulShellRoute.indexedStack( restorationScopeId: 'shell', - pageBuilder: (BuildContext context, GoRouterState state, - StatefulNavigationShell navigationShell) { - routeState = navigationShell; + pageBuilder: + (BuildContext context, ShellRouteState state, Widget child) { + routeState = state; return MaterialPage( - restorationId: 'shellWidget', child: navigationShell); + restorationId: 'shellWidget', child: child); }, branches: [ StatefulShellBranch( @@ -5245,7 +5235,7 @@ void main() { statefulWidgetKeyC.currentState?.increment(); expect(statefulWidgetKeyC.currentState?.counter, equals(1)); - routeState!.goBranch(0); + router.restore(routeState!.navigatorLocation(0)); await tester.pumpAndSettle(); expect(find.text('Screen A Detail'), findsOneWidget); @@ -5255,12 +5245,12 @@ void main() { expect(find.text('Screen A Detail'), findsOneWidget); expect(statefulWidgetKeyA.currentState?.counter, equals(1)); - routeState!.goBranch(1); + router.restore(routeState!.navigatorLocation(1)); await tester.pumpAndSettle(); expect(find.text('Screen B Detail'), findsOneWidget); expect(statefulWidgetKeyB.currentState?.counter, equals(1)); - routeState!.goBranch(2); + router.restore(routeState!.navigatorLocation(2)); await tester.pumpAndSettle(); expect(find.text('Screen C Detail'), findsOneWidget); // State of branch C should not have been restored @@ -5276,17 +5266,17 @@ void main() { GlobalKey(); final GlobalKey statefulWidgetKeyB = GlobalKey(); - StatefulNavigationShell? routeStateRoot; - StatefulNavigationShell? routeStateNested; + ShellRouteState? routeStateRoot; + ShellRouteState? routeStateNested; final List routes = [ StatefulShellRoute.indexedStack( restorationScopeId: 'shell', - pageBuilder: (BuildContext context, GoRouterState state, - StatefulNavigationShell navigationShell) { - routeStateRoot = navigationShell; + pageBuilder: + (BuildContext context, ShellRouteState state, Widget child) { + routeStateRoot = state; return MaterialPage( - restorationId: 'shellWidget', child: navigationShell); + restorationId: 'shellWidget', child: child); }, branches: [ StatefulShellBranch( @@ -5317,12 +5307,11 @@ void main() { routes: [ StatefulShellRoute.indexedStack( restorationScopeId: 'branchB-nested-shell', - pageBuilder: (BuildContext context, GoRouterState state, - StatefulNavigationShell navigationShell) { - routeStateNested = navigationShell; + pageBuilder: (BuildContext context, ShellRouteState state, + Widget child) { + routeStateNested = state; return MaterialPage( - restorationId: 'shellWidget-nested', - child: navigationShell); + restorationId: 'shellWidget-nested', child: child); }, branches: [ StatefulShellBranch( @@ -5372,7 +5361,7 @@ void main() { statefulWidgetKeyA.currentState?.increment(); expect(statefulWidgetKeyA.currentState?.counter, equals(1)); - routeStateRoot!.goBranch(1); + router.restore(routeStateRoot!.navigatorLocation(1)); await tester.pumpAndSettle(); router.go('/b/detailB'); @@ -5380,7 +5369,7 @@ void main() { statefulWidgetKeyB.currentState?.increment(); expect(statefulWidgetKeyB.currentState?.counter, equals(1)); - routeStateRoot!.goBranch(0); + router.restore(routeStateRoot!.navigatorLocation(0)); await tester.pumpAndSettle(); expect(find.text('Screen A Detail'), findsOneWidget); expect(find.text('Screen B'), findsNothing); @@ -5394,16 +5383,16 @@ void main() { expect(find.text('Screen B Pushed Detail'), findsNothing); expect(statefulWidgetKeyA.currentState?.counter, equals(1)); - routeStateRoot!.goBranch(1); + router.restore(routeStateRoot!.navigatorLocation(1)); await tester.pumpAndSettle(); expect(find.text('Screen A Detail'), findsNothing); expect(find.text('Screen B'), findsNothing); expect(find.text('Screen B Detail'), findsOneWidget); expect(statefulWidgetKeyB.currentState?.counter, equals(1)); - routeStateNested!.goBranch(1); + router.restore(routeStateNested!.navigatorLocation(1)); await tester.pumpAndSettle(); - routeStateNested!.goBranch(0); + router.restore(routeStateNested!.navigatorLocation(0)); await tester.pumpAndSettle(); expect(find.text('Screen B Detail'), findsOneWidget); diff --git a/packages/go_router/test/route_data_test.dart b/packages/go_router/test/route_data_test.dart index 1da35a672b6..779c8baf904 100644 --- a/packages/go_router/test/route_data_test.dart +++ b/packages/go_router/test/route_data_test.dart @@ -151,12 +151,12 @@ class _StatefulShellRouteDataBuilder extends StatefulShellRouteData { @override Widget builder( BuildContext context, - GoRouterState state, - StatefulNavigationShell navigator, + ShellRouteState state, + Widget child, ) => SizedBox( key: const Key('builder'), - child: navigator, + child: child, ); } @@ -181,13 +181,13 @@ class _StatefulShellRouteDataPageBuilder extends StatefulShellRouteData { @override Page pageBuilder( BuildContext context, - GoRouterState state, - StatefulNavigationShell navigator, + ShellRouteState state, + Widget child, ) => MaterialPage( child: SizedBox( key: const Key('page-builder'), - child: navigator, + child: child, ), ); } diff --git a/packages/go_router/test/test_helpers.dart b/packages/go_router/test/test_helpers.dart index e6f69c50788..30904263991 100644 --- a/packages/go_router/test/test_helpers.dart +++ b/packages/go_router/test/test_helpers.dart @@ -359,8 +359,8 @@ GoRouterPageBuilder createPageBuilder( (BuildContext context, GoRouterState state) => MaterialPage(restorationId: restorationId, child: child); -StatefulShellRouteBuilder mockStackedShellBuilder = (BuildContext context, - GoRouterState state, StatefulNavigationShell navigationShell) { +ShellRouteBuilder mockStatefulShellBuilder = + (BuildContext context, ShellRouteState state, Widget navigationShell) { return navigationShell; };