diff --git a/packages/go_router/CHANGELOG.md b/packages/go_router/CHANGELOG.md index ae73f40e0c28..ea58f2fbf03b 100644 --- a/packages/go_router/CHANGELOG.md +++ b/packages/go_router/CHANGELOG.md @@ -1,3 +1,7 @@ +## 10.2.0 + +- Adds preload support to StatefulShellRoute, configurable via `lazy` parameter on StatefulShellBranch. + ## 10.1.2 * Adds pub topics to package metadata. 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 5fbe2b6d2868..8bbd859ed7ba 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,10 @@ final GlobalKey _rootNavigatorKey = GlobalKey(debugLabel: 'root'); final GlobalKey _tabANavigatorKey = GlobalKey(debugLabel: 'tabANav'); +final GlobalKey _tabB1NavigatorKey = + GlobalKey(debugLabel: 'tabB1Nav'); +final GlobalKey _tabB2NavigatorKey = + GlobalKey(debugLabel: 'tabB2Nav'); // This example demonstrates how to setup nested navigation using a // BottomNavigationBar, where each bar item uses its own persistent navigator, @@ -80,71 +84,68 @@ class NestedTabNavigationExampleApp extends StatelessWidget { // The route branch for the third tab of the bottom navigation bar. StatefulShellBranch( + // To enable preloading of the initial locations of branches, pass + // 'true' for the parameter preload. + preload: true, // 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 // parameter. - // defaultLocation: '/c2', + // defaultLocation: '/b1', routes: [ - StatefulShellRoute( + StatefulShellRoute.indexedStack( builder: (BuildContext context, GoRouterState state, StatefulNavigationShell navigationShell) { - // Just like with the top level StatefulShellRoute, no - // customization is done in the builder function. - return navigationShell; + return TabbedRootScreen(navigationShell: navigationShell); }, - navigatorContainerBuilder: (BuildContext context, - StatefulNavigationShell navigationShell, - 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); - }, - // This bottom tab uses a nested shell, wrapping sub routes in a - // top TabBar. branches: [ - StatefulShellBranch(routes: [ - GoRoute( - path: '/b1', - builder: (BuildContext context, GoRouterState state) => - const TabScreen( - label: 'B1', detailsPath: '/b1/details'), - routes: [ + StatefulShellBranch( + navigatorKey: _tabB1NavigatorKey, + routes: [ GoRoute( - path: 'details', + path: '/b1', builder: (BuildContext context, GoRouterState state) => - const DetailsScreen( - label: 'B1', - withScaffold: false, - ), + const TabScreen( + label: 'B1', detailsPath: '/b1/details'), + routes: [ + GoRoute( + path: 'details', + builder: + (BuildContext context, GoRouterState state) => + const DetailsScreen( + label: 'B1', + withScaffold: false, + ), + ), + ], ), - ], - ), - ]), - StatefulShellBranch(routes: [ - GoRoute( - path: '/b2', - builder: (BuildContext context, GoRouterState state) => - const TabScreen( - label: 'B2', detailsPath: '/b2/details'), - routes: [ + ]), + StatefulShellBranch( + navigatorKey: _tabB2NavigatorKey, + // To enable preloading for all nested branches, set + // preload to 'true'. + preload: true, + routes: [ GoRoute( - path: 'details', + path: '/b2', builder: (BuildContext context, GoRouterState state) => - const DetailsScreen( - label: 'B2', - withScaffold: false, - ), + const TabScreen( + label: 'B2', detailsPath: '/b2/details'), + routes: [ + GoRoute( + path: 'details', + builder: + (BuildContext context, GoRouterState state) => + const DetailsScreen( + label: 'B2', + withScaffold: false, + ), + ), + ], ), - ], - ), - ]), + ]), ], ), ], @@ -376,23 +377,20 @@ class DetailsScreenState extends State { /// Builds a nested shell using a [TabBar] and [TabBarView]. class TabbedRootScreen extends StatefulWidget { /// Constructs a TabbedRootScreen - const TabbedRootScreen( - {required this.navigationShell, required this.children, super.key}); + const TabbedRootScreen({required this.navigationShell, super.key}); /// The current state of the parent StatefulShellRoute. final StatefulNavigationShell navigationShell; - /// The children (branch Navigators) to display in the [TabBarView]. - final List children; - @override State createState() => _TabbedRootScreenState(); } class _TabbedRootScreenState extends State with SingleTickerProviderStateMixin { + late final int branchCount = widget.navigationShell.route.branches.length; late final TabController _tabController = TabController( - length: widget.children.length, + length: branchCount, vsync: this, initialIndex: widget.navigationShell.currentIndex); @@ -404,9 +402,9 @@ class _TabbedRootScreenState extends State @override Widget build(BuildContext context) { - final List tabs = widget.children - .mapIndexed((int i, _) => Tab(text: 'Tab ${i + 1}')) - .toList(); + final List tabs = + List.generate(branchCount, (int i) => Tab(text: 'Tab ${i + 1}')) + .toList(); return Scaffold( appBar: AppBar( @@ -416,10 +414,7 @@ class _TabbedRootScreenState extends State tabs: tabs, onTap: (int tappedIndex) => _onTabTap(context, tappedIndex), )), - body: TabBarView( - controller: _tabController, - children: widget.children, - ), + body: widget.navigationShell, ); } @@ -441,6 +436,11 @@ class TabScreen extends StatelessWidget { @override Widget build(BuildContext context) { + /// If preloading is enabled on the top StatefulShellRoute, this will be + /// printed directly after the app has been started, but only for the route + /// that is the initial location ('/b1') + debugPrint('Building TabScreen - $label'); + return Center( child: Column( mainAxisSize: MainAxisSize.min, diff --git a/packages/go_router/lib/src/builder.dart b/packages/go_router/lib/src/builder.dart index 1e46417b30f1..836a0cfeb951 100644 --- a/packages/go_router/lib/src/builder.dart +++ b/packages/go_router/lib/src/builder.dart @@ -216,11 +216,15 @@ class RouteBuilder { final RouteBase route = match.route; final GoRouterState state = buildState(matchList, match); Page? page; + + void buildNext(RouteMatchList matches, GlobalKey key) => + _buildRecursive(context, matches, startIndex + 1, pagePopContext, + routerNeglect, keyToPages, key, registry); + if (state.error != null) { page = _buildErrorPage(context, state); keyToPages.putIfAbsent(navigatorKey, () => >[]).add(page); - _buildRecursive(context, matchList, startIndex + 1, pagePopContext, - routerNeglect, keyToPages, navigatorKey, registry); + buildNext(matchList, navigatorKey); } else { // If this RouteBase is for a different Navigator, add it to the // list of out of scope pages @@ -236,8 +240,7 @@ class RouteBuilder { .add(page); } - _buildRecursive(context, matchList, startIndex + 1, pagePopContext, - routerNeglect, keyToPages, navigatorKey, registry); + buildNext(matchList, navigatorKey); } else if (route is ShellRouteBase) { assert(startIndex + 1 < matchList.matches.length, 'Shell routes must always have child routes'); @@ -258,25 +261,40 @@ class RouteBuilder { keyToPages.putIfAbsent(shellNavigatorKey, () => >[]); // Build the remaining pages - _buildRecursive(context, matchList, startIndex + 1, pagePopContext, - routerNeglect, keyToPages, shellNavigatorKey, registry); + buildNext(matchList, shellNavigatorKey); - final HeroController heroController = _goHeroCache.putIfAbsent( + _goHeroCache.putIfAbsent( shellNavigatorKey, () => _getHeroController(context)); // Build the Navigator for this shell route Widget buildShellNavigator( + GlobalKey navigatorKey, List? observers, String? restorationScopeId, { bool requestFocus = true, + RouteMatchList? preloadedNavigatorMatches, }) { + if (preloadedNavigatorMatches != null && + keyToPages[navigatorKey] == null) { + // Preloaded RouteMatchList must point to a route that is a + // descendant of the current shell route. + assert( + preloadedNavigatorMatches.matches[startIndex].route == route); + + // Build the pages for this preloadable navigator + buildNext(preloadedNavigatorMatches, navigatorKey); + + _goHeroCache.putIfAbsent( + navigatorKey, () => _getHeroController(context)); + } + return _buildNavigator( pagePopContext.onPopPage, - keyToPages[shellNavigatorKey]!, - shellNavigatorKey, + keyToPages[navigatorKey]!, + navigatorKey, observers: observers ?? const [], restorationScopeId: restorationScopeId, - heroController: heroController, + heroController: _goHeroCache[navigatorKey], requestFocus: requestFocus, ); } diff --git a/packages/go_router/lib/src/route.dart b/packages/go_router/lib/src/route.dart index 7c02236ea02e..9b27a0992629 100644 --- a/packages/go_router/lib/src/route.dart +++ b/packages/go_router/lib/src/route.dart @@ -55,7 +55,12 @@ typedef StatefulShellRoutePageBuilder = Page Function( /// Signature for functions used to build Navigators typedef NavigatorBuilder = Widget Function( - List? observers, String? restorationScopeId); + GlobalKey navigatorKey, + List? observers, + String? restorationScopeId, { + bool requestFocus, + RouteMatchList? preloadedNavigatorMatches, +}); /// The base class for [GoRoute] and [ShellRoute]. /// @@ -464,12 +469,12 @@ class ShellRouteContext { /// The current route state associated with [route]. final GoRouterState routerState; - /// The [Navigator] key to be used for the nested navigation associated with + /// The [Navigator] key of the current Navigator within the associated /// [route]. final GlobalKey navigatorKey; - /// The route match list representing the current location within the - /// associated shell route. + /// The route match list representing the location of the current [Navigator] + /// within the associated [route]. final RouteMatchList routeMatchList; /// Function used to build the [Navigator] for the current route. @@ -611,8 +616,8 @@ class ShellRoute extends ShellRouteBase { Widget? buildWidget(BuildContext context, GoRouterState state, ShellRouteContext shellRouteContext) { if (builder != null) { - final Widget navigator = - shellRouteContext.navigatorBuilder(observers, restorationScopeId); + final Widget navigator = shellRouteContext.navigatorBuilder( + shellRouteContext.navigatorKey, observers, restorationScopeId); return builder!(context, state, navigator); } return null; @@ -622,8 +627,8 @@ class ShellRoute extends ShellRouteBase { Page? buildPage(BuildContext context, GoRouterState state, ShellRouteContext shellRouteContext) { if (pageBuilder != null) { - final Widget navigator = - shellRouteContext.navigatorBuilder(observers, restorationScopeId); + final Widget navigator = shellRouteContext.navigatorBuilder( + shellRouteContext.navigatorKey, observers, restorationScopeId); return pageBuilder!(context, state, navigator); } return null; @@ -750,6 +755,9 @@ class StatefulShellRoute extends ShellRouteBase { 'Navigator keys must be unique'), assert(_debugValidateParentNavigatorKeys(branches)), assert(_debugValidateRestorationScopeIds(restorationScopeId, branches)), + _preloadableBranches = branches + .where((StatefulShellBranch b) => b.preload) + .toList(growable: false), super._(routes: _routes(branches)); /// Constructs a StatefulShellRoute that uses an [IndexedStack] for its @@ -830,6 +838,9 @@ class StatefulShellRoute extends ShellRouteBase { /// [StatefulShellBranch.navigatorKey]. final List branches; + /// Cached list of branches that should be preloaded. + final List _preloadableBranches; + final GlobalKey _shellStateKey = GlobalKey(); @@ -865,11 +876,40 @@ class StatefulShellRoute extends ShellRouteBase { branches.map((StatefulShellBranch b) => b.navigatorKey); StatefulNavigationShell _createShell( - BuildContext context, ShellRouteContext shellRouteContext) => - StatefulNavigationShell( - shellRouteContext: shellRouteContext, - router: GoRouter.of(context), - containerBuilder: navigatorContainerBuilder); + BuildContext context, ShellRouteContext shellRouteContext) { + final GoRouter router = GoRouter.of(context); + final List<_PreloadedStatefulShellBranch> preloadData = + _preloadBranches(shellRouteContext, router); + return StatefulNavigationShell._( + shellRouteContext: shellRouteContext, + router: router, + containerBuilder: navigatorContainerBuilder, + preloadedBranches: preloadData, + ); + } + + List<_PreloadedStatefulShellBranch> _preloadBranches( + ShellRouteContext shellRouteContext, GoRouter router) { + final List<_PreloadedStatefulShellBranch> preloadData = + <_PreloadedStatefulShellBranch>[]; + final StatefulNavigationShellState? shellState = + _shellStateKey.currentState; + + for (final StatefulShellBranch branch in _preloadableBranches) { + final bool loaded = shellState?._isBranchLoaded(branch) ?? false; + if (!loaded) { + final RouteMatchList matchList = router.configuration.findMatch( + branch._effectiveInitialBranchLocation(router.configuration)); + final Widget navigator = shellRouteContext.navigatorBuilder( + branch.navigatorKey, branch.observers, branch.restorationScopeId, + preloadedNavigatorMatches: matchList); + preloadData + .add(_PreloadedStatefulShellBranch(branch, matchList, navigator)); + } + } + + return preloadData; + } static Widget _indexedStackContainerBuilder(BuildContext context, StatefulNavigationShell navigationShell, List children) { @@ -921,6 +961,15 @@ class StatefulShellRoute extends ShellRouteBase { } } +/// Data structure used for preloading a [StatefulShellBranch]. +class _PreloadedStatefulShellBranch { + _PreloadedStatefulShellBranch(this.branch, this.matchList, this.navigator); + + final StatefulShellBranch branch; + final RouteMatchList matchList; + final Widget navigator; +} + /// Representation of a separate branch in a stateful navigation tree, used to /// configure [StatefulShellRoute]. /// @@ -945,6 +994,7 @@ class StatefulShellBranch { this.initialLocation, this.restorationScopeId, this.observers, + this.preload = false, }) : navigatorKey = navigatorKey ?? GlobalKey() { assert(() { ShellRouteBase._debugCheckSubRouteParentNavigatorKeys( @@ -981,12 +1031,39 @@ class StatefulShellBranch { /// The observers parameter is used by the [Navigator] built for this branch. final List? observers; + /// Whether this route branch should be loaded only when navigating to it for + /// the first time (the default behavior, i.e. 'false'), or if it should be + /// eagerly loaded (preloaded). + /// + /// If this property is true, the branch will be loaded immediately when the + /// associated [StatefulShellRoute] is visited for the first time. In that + /// case, the branch will be preloaded by navigating to the initial location + /// (see [initialLocation]). + /// + /// *Note:* The primary purpose of branch preloading is to enhance the user + /// experience when switching branches, which might for instance involve + /// preparing the UI for animated transitions etc. Care must be taken to + /// **keep the preloading to an absolute minimum** to avoid any unnecessary + /// resource use. + final bool preload; + /// The default route of this branch, i.e. the first descendant [GoRoute]. /// /// This route will be used when loading the branch for the first time, if /// an [initialLocation] has not been provided. GoRoute? get defaultRoute => RouteBase.routesRecursively(routes).whereType().firstOrNull; + + String _effectiveInitialBranchLocation( + RouteConfiguration routeConfiguration) { + 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. + return routeConfiguration.locationForRoute(defaultRoute!)!; + } + } } /// Builder for a custom container for the branch Navigators of a @@ -1015,12 +1092,25 @@ typedef ShellNavigationContainerBuilder = Widget Function(BuildContext context, /// ``` class StatefulNavigationShell extends StatefulWidget { /// Constructs an [StatefulNavigationShell]. + // TODO(tolo): There shouldn't be a need for a public constructor on StatefulNavigationShell, consider removing it (technically a breaking change). StatefulNavigationShell({ + required ShellRouteContext shellRouteContext, + required GoRouter router, + required ShellNavigationContainerBuilder containerBuilder, + }) : this._( + shellRouteContext: shellRouteContext, + router: router, + containerBuilder: containerBuilder, + preloadedBranches: <_PreloadedStatefulShellBranch>[]); + + StatefulNavigationShell._({ required this.shellRouteContext, required GoRouter router, required this.containerBuilder, + required List<_PreloadedStatefulShellBranch> preloadedBranches, }) : assert(shellRouteContext.route is StatefulShellRoute), _router = router, + _preloadedBranches = preloadedBranches, currentIndex = _indexOfBranchNavigatorKey( shellRouteContext.route as StatefulShellRoute, shellRouteContext.navigatorKey), @@ -1032,6 +1122,8 @@ class StatefulNavigationShell extends StatefulWidget { /// current [StatefulShellBranch]. final ShellRouteContext shellRouteContext; + final List<_PreloadedStatefulShellBranch> _preloadedBranches; + final GoRouter _router; /// The builder for a custom container for shell route Navigators. @@ -1076,15 +1168,7 @@ class StatefulNavigationShell extends StatefulWidget { 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!; - return _router.configuration.locationForRoute(route)!; - } + return branch._effectiveInitialBranchLocation(_router.configuration); } @override @@ -1157,6 +1241,9 @@ class StatefulNavigationShellState extends State RouteMatchList? _matchListForBranch(int index) => _branchLocations[route.branches[index]]?.value; + bool _isBranchLoaded(StatefulShellBranch branch) => + _branchNavigators[branch.navigatorKey] != null; + /// Creates a new RouteMatchList that is scoped to the Navigators of the /// current shell route or it's descendants. This involves removing all the /// trailing imperative matches from the RouterMatchList that are targeted at @@ -1185,6 +1272,8 @@ class StatefulNavigationShellState extends State } void _updateCurrentBranchStateFromWidget() { + _preloadBranches(); + final StatefulShellBranch branch = route.branches[widget.currentIndex]; final ShellRouteContext shellRouteContext = widget.shellRouteContext; final RouteMatchList currentBranchLocation = @@ -1201,8 +1290,19 @@ class StatefulNavigationShellState extends State final bool locationChanged = previousBranchLocation != currentBranchLocation; if (locationChanged || !hasExistingNavigator) { - _branchNavigators[branch.navigatorKey] = shellRouteContext - .navigatorBuilder(branch.observers, branch.restorationScopeId); + _branchNavigators[branch.navigatorKey] = + shellRouteContext.navigatorBuilder( + branch.navigatorKey, branch.observers, branch.restorationScopeId); + } + } + + void _preloadBranches() { + for (final _PreloadedStatefulShellBranch preloaded + in widget._preloadedBranches) { + if (!_isBranchLoaded(preloaded.branch)) { + _branchLocation(preloaded.branch, false).value = preloaded.matchList; + _branchNavigators[preloaded.branch.navigatorKey] = preloaded.navigator; + } } } diff --git a/packages/go_router/pubspec.yaml b/packages/go_router/pubspec.yaml index b70f0b23407b..128569f1f1a1 100644 --- a/packages/go_router/pubspec.yaml +++ b/packages/go_router/pubspec.yaml @@ -1,7 +1,7 @@ name: go_router description: A declarative router for Flutter based on Navigation 2 supporting deep linking, data-driven routes and more -version: 10.1.2 +version: 10.2.0 repository: https://github.com/flutter/packages/tree/main/packages/go_router issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+go_router%22 diff --git a/packages/go_router/test/go_router_test.dart b/packages/go_router/test/go_router_test.dart index 755b3e3b9bd6..eeb2247a45fe 100644 --- a/packages/go_router/test/go_router_test.dart +++ b/packages/go_router/test/go_router_test.dart @@ -3892,6 +3892,179 @@ void main() { expect(find.text('Common - X'), findsOneWidget); }); + testWidgets('Preloads routes correctly in a StatefulShellRoute', + (WidgetTester tester) async { + final GlobalKey rootNavigatorKey = + GlobalKey(); + final GlobalKey statefulWidgetKeyA = + GlobalKey(debugLabel: 'A'); + final GlobalKey statefulWidgetKeyB = + GlobalKey(debugLabel: 'B'); + final GlobalKey statefulWidgetKeyC = + GlobalKey(debugLabel: 'C'); + final GlobalKey statefulWidgetKeyD = + GlobalKey(debugLabel: 'D'); + final GlobalKey statefulWidgetKeyE = + GlobalKey(debugLabel: 'E'); + + final List routes = [ + StatefulShellRoute.indexedStack( + builder: mockStackedShellBuilder, + branches: [ + StatefulShellBranch(routes: [ + GoRoute( + path: '/a', + builder: (BuildContext context, GoRouterState state) => + DummyStatefulWidget(key: statefulWidgetKeyA), + ), + ]), + StatefulShellBranch(routes: [ + GoRoute( + path: '/b', + builder: (BuildContext context, GoRouterState state) => + DummyStatefulWidget(key: statefulWidgetKeyB), + ), + ]), + ], + ), + StatefulShellRoute.indexedStack( + builder: mockStackedShellBuilder, + branches: [ + StatefulShellBranch( + preload: true, + routes: [ + GoRoute( + path: '/c', + builder: (BuildContext context, GoRouterState state) => + DummyStatefulWidget(key: statefulWidgetKeyC), + ), + ], + ), + StatefulShellBranch( + preload: true, + routes: [ + GoRoute( + path: '/d', + builder: (BuildContext context, GoRouterState state) => + DummyStatefulWidget(key: statefulWidgetKeyD), + ), + ], + ), + StatefulShellBranch( + preload: true, + initialLocation: '/e/details', + routes: [ + GoRoute( + path: '/e', + builder: (BuildContext context, GoRouterState state) => + const Text('E'), + routes: [ + GoRoute( + path: 'details', + builder: (BuildContext context, GoRouterState state) => + DummyStatefulWidget(key: statefulWidgetKeyE), + ), + ]), + ], + ), + ], + ), + ]; + + final GoRouter router = await createRouter( + routes, + tester, + initialLocation: '/a', + navigatorKey: rootNavigatorKey, + ); + expect(statefulWidgetKeyA.currentState?.counter, equals(0)); + expect(statefulWidgetKeyB.currentState?.counter, null); + expect(statefulWidgetKeyC.currentState?.counter, null); + expect(statefulWidgetKeyD.currentState?.counter, null); + + router.go('/c'); + await tester.pumpAndSettle(); + expect(statefulWidgetKeyC.currentState?.counter, equals(0)); + expect(statefulWidgetKeyD.currentState?.counter, equals(0)); + expect(statefulWidgetKeyE.currentState?.counter, equals(0)); + }); + + testWidgets('Preloads nested routes correctly in a StatefulShellRoute', + (WidgetTester tester) async { + final GlobalKey rootNavigatorKey = + GlobalKey(); + final GlobalKey statefulWidgetKeyA = + GlobalKey(debugLabel: 'A'); + final GlobalKey statefulWidgetKeyB = + GlobalKey(debugLabel: 'B'); + final GlobalKey statefulWidgetKeyC = + GlobalKey(debugLabel: 'C'); + final GlobalKey statefulWidgetKeyD = + GlobalKey(debugLabel: 'D'); + + final List routes = [ + StatefulShellRoute.indexedStack( + builder: mockStackedShellBuilder, + branches: [ + StatefulShellBranch( + preload: true, + routes: [ + StatefulShellRoute.indexedStack( + builder: mockStackedShellBuilder, + branches: [ + StatefulShellBranch(preload: true, routes: [ + GoRoute( + path: '/a', + builder: (BuildContext context, GoRouterState state) => + DummyStatefulWidget(key: statefulWidgetKeyA), + ), + ]), + StatefulShellBranch(preload: true, routes: [ + GoRoute( + path: '/b', + builder: (BuildContext context, GoRouterState state) => + DummyStatefulWidget(key: statefulWidgetKeyB), + ), + ]), + ], + ), + ], + ), + StatefulShellBranch( + preload: true, + routes: [ + GoRoute( + path: '/c', + builder: (BuildContext context, GoRouterState state) => + DummyStatefulWidget(key: statefulWidgetKeyC), + ), + ], + ), + StatefulShellBranch( + routes: [ + GoRoute( + path: '/d', + builder: (BuildContext context, GoRouterState state) => + DummyStatefulWidget(key: statefulWidgetKeyD), + ), + ], + ), + ], + ), + ]; + + await createRouter( + routes, + tester, + initialLocation: '/c', + navigatorKey: rootNavigatorKey, + ); + expect(statefulWidgetKeyA.currentState?.counter, equals(0)); + expect(statefulWidgetKeyB.currentState?.counter, equals(0)); + expect(statefulWidgetKeyC.currentState?.counter, equals(0)); + expect(statefulWidgetKeyD.currentState?.counter, null); + }); + testWidgets( 'Redirects are correctly handled when switching branch in a ' 'StatefulShellRoute', (WidgetTester tester) async {