From fa4c54296f7a0afed90c8dbf891aceacfe1a93ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20Lo=CC=88fstrand?= Date: Mon, 26 Sep 2022 15:28:39 +0200 Subject: [PATCH 001/112] Added NestedNavigationShellRoute, to support building nested persistent navigation. --- packages/go_router/lib/go_router.dart | 2 +- packages/go_router/lib/src/builder.dart | 13 +++++++++--- packages/go_router/lib/src/delegate.dart | 4 ++++ packages/go_router/lib/src/route.dart | 27 ++++++++++++++++++++++++ packages/go_router/lib/src/typedefs.dart | 7 ++++++ 5 files changed, 49 insertions(+), 4 deletions(-) diff --git a/packages/go_router/lib/go_router.dart b/packages/go_router/lib/go_router.dart index 1d9f9dd8e15d..93ea28386a62 100644 --- a/packages/go_router/lib/go_router.dart +++ b/packages/go_router/lib/go_router.dart @@ -7,7 +7,7 @@ library go_router; export 'src/configuration.dart' - show GoRoute, GoRouterState, RouteBase, ShellRoute; + show GoRoute, GoRouterState, RouteBase, ShellRoute, NestedNavigationShellRoute; export 'src/misc/extensions.dart'; export 'src/misc/inherited_router.dart'; export 'src/pages/custom_transition_page.dart'; diff --git a/packages/go_router/lib/src/builder.dart b/packages/go_router/lib/src/builder.dart index 6170984dc2cd..808b4490578f 100644 --- a/packages/go_router/lib/src/builder.dart +++ b/packages/go_router/lib/src/builder.dart @@ -187,9 +187,16 @@ class RouteBuilder { _buildRecursive(context, matchList, startIndex + 1, pop, routerNeglect, keyToPages, newParams, shellNavigatorKey); - // Build the Navigator - final Widget child = _buildNavigator( - pop, keyToPages[shellNavigatorKey]!, shellNavigatorKey); + final Widget child; + if (route is NestedNavigationShellRoute) { + // Build the container for the nested routes + child = route.nestedNavigationBuilder(context, state, + keyToPages[shellNavigatorKey]!); + } else { + // Build the Navigator + child = _buildNavigator( + pop, keyToPages[shellNavigatorKey]!, shellNavigatorKey); + } // Build the Page for this route final Page page = diff --git a/packages/go_router/lib/src/delegate.dart b/packages/go_router/lib/src/delegate.dart index f2ee938629e7..6ffc24ab4ad8 100644 --- a/packages/go_router/lib/src/delegate.dart +++ b/packages/go_router/lib/src/delegate.dart @@ -121,6 +121,10 @@ class GoRouterDelegate extends RouterDelegate // in this case. assert(canPop); return canPop; + } else if (route is NestedNavigationShellRoute) { + // NestedNavigationShellRoute delegates navigation handling and should + // therefore not handle pop here + continue; } else if (route is ShellRoute) { final bool canPop = route.navigatorKey.currentState!.canPop(); if (canPop) { diff --git a/packages/go_router/lib/src/route.dart b/packages/go_router/lib/src/route.dart index 297ce88d69dd..884e0d66203a 100644 --- a/packages/go_router/lib/src/route.dart +++ b/packages/go_router/lib/src/route.dart @@ -464,3 +464,30 @@ class ShellRoute extends RouteBase { /// are placed onto this Navigator instead of the root Navigator. final GlobalKey navigatorKey; } + + +/// Specialized version of [ShellRoute], that delegates the building of nested +/// [Navigator]s to a child [Widget], created by +class NestedNavigationShellRoute extends ShellRoute { + + /// Constructs a [NestedNavigationShellRoute]. + NestedNavigationShellRoute({ + required this.nestedNavigationBuilder, + required super.routes, + ShellRoutePageBuilder? pageBuilder, + }) : super(pageBuilder: pageBuilder ?? _defaultPageBuilder); + + /// The widget builder for a nested navigation shell route. + /// + /// Similar to GoRoute builder, but with an additional pages parameter. This + /// pages parameter contains the pages that should currently be displayed in + /// a nested navigation. + final ShellRouteNestedNavigationBuilder nestedNavigationBuilder; + + static Page _defaultPageBuilder(BuildContext context, + GoRouterState state, Widget child) { + return NoTransitionPage(child: child, name: state.name ?? state.fullpath, + restorationId: state.pageKey.value); + } + +} diff --git a/packages/go_router/lib/src/typedefs.dart b/packages/go_router/lib/src/typedefs.dart index f89448876b36..a6bef39d2eaf 100644 --- a/packages/go_router/lib/src/typedefs.dart +++ b/packages/go_router/lib/src/typedefs.dart @@ -34,6 +34,13 @@ typedef ShellRoutePageBuilder = Page Function( Widget child, ); +/// The builder for a [NestedNavigationShellRoute] +typedef ShellRouteNestedNavigationBuilder = Widget Function( + BuildContext context, + GoRouterState state, + List> pages, +); + /// The signature of the navigatorBuilder callback. typedef GoRouterNavigatorBuilder = Widget Function( BuildContext context, From 76b17ae89f8293cf0015bce466d3b5b52a8d1d1c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20Lo=CC=88fstrand?= Date: Mon, 26 Sep 2022 15:28:57 +0200 Subject: [PATCH 002/112] Added example for NestedNavigationShellRoute. --- .../lib/nested_navigation_shell_route.dart | 343 ++++++++++++++++++ 1 file changed, 343 insertions(+) create mode 100644 packages/go_router/example/lib/nested_navigation_shell_route.dart diff --git a/packages/go_router/example/lib/nested_navigation_shell_route.dart b/packages/go_router/example/lib/nested_navigation_shell_route.dart new file mode 100644 index 000000000000..3a2b29c6a1f6 --- /dev/null +++ b/packages/go_router/example/lib/nested_navigation_shell_route.dart @@ -0,0 +1,343 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; + + +// This example demonstrates how to setup nested navigation using a +// BottomNavigationBar, where each tab uses its own persistent navigator, i.e. +// navigation state is maintained separately for each tab. This setup also +// enables deep linking into nested pages. +// +// The example is loosely based on the ShellRoute sample in go_router, but +// differs in that it is able to maintain the navigation state of each tab. +// This example introduces a fex (imperfect) classes that ideally should be part +// of go_router, such as BottomTabBarShellRoute etc. + +void main() { + runApp(NestedTabNavigationExampleApp()); +} + + +/// NestedNavigationShellRoute that uses a bottom tab navigation +/// (ScaffoldWithNavBar) with separate navigators for each tab. +class BottomTabBarShellRoute extends NestedNavigationShellRoute { + /// Constructs a BottomTabBarShellRoute + BottomTabBarShellRoute({ + required this.tabs, + List routes = const [], + Key? scaffoldKey = const ValueKey('ScaffoldWithNavBar'), + }) : super( + routes: routes, + nestedNavigationBuilder: (BuildContext context, GoRouterState state, + List> pagesForCurrentRoute) { + return ScaffoldWithNavBar(tabs: tabs, key: scaffoldKey, + pagesForCurrentRoute: pagesForCurrentRoute); + } + ); + + /// The tabs + final List tabs; +} + + +/// An example demonstrating how to use nested navigators +class NestedTabNavigationExampleApp extends StatelessWidget { + /// Creates a NestedTabNavigationExampleApp + NestedTabNavigationExampleApp({Key? key}) : super(key: key); + + static const List _tabs = + [ + ScaffoldWithNavBarTabItem(initialLocation: '/a', + icon: Icon(Icons.home), label: 'Section A'), + ScaffoldWithNavBarTabItem(initialLocation: '/b', + icon: Icon(Icons.settings), label: 'Section B'), + ]; + + final GoRouter _router = GoRouter( + initialLocation: '/a', + navigatorKey: GlobalKey(debugLabel: 'Root'), + routes: [ + /// Custom shell route - wraps the below routes in a scaffold with + /// a bottom tab navigator + BottomTabBarShellRoute( + tabs: _tabs, + routes: [ + /// The screen to display as the root in the first tab of the bottom + /// navigation bar. + GoRoute( + path: '/a', + builder: (BuildContext context, GoRouterState state) => + const RootScreen(label: 'A', detailsPath: '/a/details'), + routes: [ + /// The details screen to display stacked on navigator of the + /// first tab. This will cover screen A but not the application + /// shell (bottom navigation bar). + GoRoute( + path: 'details', + builder: (BuildContext context, GoRouterState state) => + const DetailsScreen(label: 'A'), + ), + ], + ), + /// The screen to display as the root in the second tab of the bottom + /// navigation bar. + GoRoute( + path: '/b', + builder: (BuildContext context, GoRouterState state) => + const RootScreen(label: 'B', detailsPath: '/b/details'), + routes: [ + /// The details screen to display stacked on navigator of the + /// second tab. This will cover screen B but not the application + /// shell (bottom navigation bar). + GoRoute( + path: 'details', + builder: (BuildContext context, GoRouterState state) => + const DetailsScreen(label: 'B'), + ), + ], + ), + ], + ), + ], + ); + + @override + Widget build(BuildContext context) { + return MaterialApp.router( + title: 'Flutter Demo', + theme: ThemeData( + primarySwatch: Colors.blue, + ), + routeInformationParser: _router.routeInformationParser, + routerDelegate: _router.routerDelegate, + routeInformationProvider: _router.routeInformationProvider, + ); + } +} + + +/// Representation of a tab item in a [ScaffoldWithNavBar] +class ScaffoldWithNavBarTabItem extends BottomNavigationBarItem { + /// Constructs an [ScaffoldWithNavBarTabItem]. + const ScaffoldWithNavBarTabItem({required this.initialLocation, + this.navigatorKey, required Widget icon, String? label}) : + super(icon: icon, label: label); + + /// The initial location/path + final String initialLocation; + + /// Optional navigatorKey + final GlobalKey? navigatorKey; +} + + +/// Builds the "shell" for the app by building a Scaffold with a +/// BottomNavigationBar, where [child] is placed in the body of the Scaffold. +class ScaffoldWithNavBar extends StatefulWidget { + /// Constructs an [ScaffoldWithNavBar]. + const ScaffoldWithNavBar({ + required this.pagesForCurrentRoute, + required this.tabs, + Key? key, + }) : super(key: key ?? const ValueKey('ScaffoldWithNavBar')); + + /// The widget to display in the body of the Scaffold. + /// In this sample, it is a Navigator. + final List> pagesForCurrentRoute; + + /// The tabs + final List tabs; + + @override + State createState() => ScaffoldWithNavBarState(); +} + +/// State for ScaffoldWithNavBar +class ScaffoldWithNavBarState extends State { + + late final List<_NavBarTabNavigator> _tabs; + + int _locationToTabIndex(String location) { + final int index = _tabs.indexWhere((_NavBarTabNavigator t) => + location.startsWith(t.initialLocation)); + return index < 0 ? 0 : index; + } + + int get _currentIndex => _locationToTabIndex(GoRouter.of(context).location); + + @override + void initState() { + super.initState(); + _tabs = widget.tabs.map((ScaffoldWithNavBarTabItem e) => + _NavBarTabNavigator(e)).toList(); + } + + @override + void didUpdateWidget(covariant ScaffoldWithNavBar oldWidget) { + super.didUpdateWidget(oldWidget); + final GoRouter route = GoRouter.of(context); + final String location = route.location; + + final int tabIndex = _locationToTabIndex(location); + + final _NavBarTabNavigator tabNav = _tabs[tabIndex]; + final List> filteredPages = widget.pagesForCurrentRoute + .where((Page p) => p.name!.startsWith(tabNav.initialLocation)) + .toList(); + + if (filteredPages.length == 1 && location != tabNav.initialLocation) { + final int index = tabNav.pages.indexWhere((Page e) => e.name == location); + if (index < 0) { + tabNav.pages.add(filteredPages.last); + } else { + tabNav.pages.length = index + 1; + } + } else { + tabNav.pages = filteredPages; + } + } + + @override + Widget build(BuildContext context) { + return Scaffold( + body: _buildBody(context), + bottomNavigationBar: BottomNavigationBar( + items: _tabs.map((_NavBarTabNavigator e) => e.bottomNavigationTab).toList(), + currentIndex: _currentIndex, + onTap: (int idx) => _onItemTapped(idx, context), + ), + ); + } + + Widget _buildBody(BuildContext context) { + return IndexedStack( + index: _currentIndex, + children: _tabs.map((_NavBarTabNavigator tab) => tab.buildNavigator(context)).toList() + ); + } + + void _onItemTapped(int index, BuildContext context) { + GoRouter.of(context).go(_tabs[index].currentLocation); + } +} + + +/// Class representing +class _NavBarTabNavigator { + + _NavBarTabNavigator(this.bottomNavigationTab); + static const String _initialPlaceholderPageName = '#placeholder#'; + + final ScaffoldWithNavBarTabItem bottomNavigationTab; + String get initialLocation => bottomNavigationTab.initialLocation; + Key? get navigatorKey => bottomNavigationTab.navigatorKey; + List> pages = >[]; + + List> get _pagesWithPlaceholder => pages.isNotEmpty ? pages : + >[const MaterialPage(name: _initialPlaceholderPageName, + child: SizedBox.shrink())]; + + String get currentLocation => pages.isNotEmpty ? pages.last.name! : initialLocation; + + Widget buildNavigator(BuildContext context) { + return Navigator( + key: navigatorKey, + pages: _pagesWithPlaceholder, + onPopPage: (Route route, dynamic result) { + if (pages.length == 1 || !route.didPop(result)) { + return false; + } + GoRouter.of(context).pop(); + return true; + }, + ); + } +} + + +/// Widget for the root/initial pages in the bottom navigation bar. +class RootScreen extends StatelessWidget { + /// Creates a RootScreen + const RootScreen({required this.label, required this.detailsPath, Key? key}) : + super(key: key); + + /// The label + final String label; + + /// The path to the detail page + final String detailsPath; + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text('Tab root - $label'), + ), + body: Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text('Screen $label', style: Theme.of(context).textTheme.titleLarge), + const Padding(padding: EdgeInsets.all(4)), + TextButton( + onPressed: () { + GoRouter.of(context).go(detailsPath); + }, + child: const Text('View details'), + ), + ], + ), + ), + ); + } +} + + +/// The details screen for either the A or B screen. +class DetailsScreen extends StatefulWidget { + + /// Constructs a [DetailsScreen]. + const DetailsScreen({ + required this.label, + Key? key, + }) : super(key: key); + + /// The label to display in the center of the screen. + final String label; + + @override + State createState() => DetailsScreenState(); +} + +/// The state for DetailsScreen +class DetailsScreenState extends State { + int _counter = 0; + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text('Details Screen - ${widget.label}'), + ), + body: Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text('Details for ${widget.label} - Counter: $_counter', + style: Theme.of(context).textTheme.titleLarge), + const Padding(padding: EdgeInsets.all(4)), + TextButton( + onPressed: () { + setState(() { _counter++; }); + }, + child: const Text('Increment counter'), + ), + ], + ), + ), + ); + } +} From 7612a5564c130456481221ccccfe07750ca40211 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20Lo=CC=88fstrand?= Date: Tue, 27 Sep 2022 10:47:43 +0200 Subject: [PATCH 003/112] Formatting. --- packages/go_router/lib/go_router.dart | 7 ++++++- packages/go_router/lib/src/builder.dart | 4 ++-- packages/go_router/lib/src/route.dart | 11 +++++------ 3 files changed, 13 insertions(+), 9 deletions(-) diff --git a/packages/go_router/lib/go_router.dart b/packages/go_router/lib/go_router.dart index 93ea28386a62..fcedbac0a475 100644 --- a/packages/go_router/lib/go_router.dart +++ b/packages/go_router/lib/go_router.dart @@ -7,7 +7,12 @@ library go_router; export 'src/configuration.dart' - show GoRoute, GoRouterState, RouteBase, ShellRoute, NestedNavigationShellRoute; + show + GoRoute, + GoRouterState, + RouteBase, + ShellRoute, + NestedNavigationShellRoute; export 'src/misc/extensions.dart'; export 'src/misc/inherited_router.dart'; export 'src/pages/custom_transition_page.dart'; diff --git a/packages/go_router/lib/src/builder.dart b/packages/go_router/lib/src/builder.dart index 808b4490578f..24a2e8bae9c0 100644 --- a/packages/go_router/lib/src/builder.dart +++ b/packages/go_router/lib/src/builder.dart @@ -190,8 +190,8 @@ class RouteBuilder { final Widget child; if (route is NestedNavigationShellRoute) { // Build the container for the nested routes - child = route.nestedNavigationBuilder(context, state, - keyToPages[shellNavigatorKey]!); + child = route.nestedNavigationBuilder( + context, state, keyToPages[shellNavigatorKey]!); } else { // Build the Navigator child = _buildNavigator( diff --git a/packages/go_router/lib/src/route.dart b/packages/go_router/lib/src/route.dart index 884e0d66203a..33e05348026f 100644 --- a/packages/go_router/lib/src/route.dart +++ b/packages/go_router/lib/src/route.dart @@ -465,11 +465,9 @@ class ShellRoute extends RouteBase { final GlobalKey navigatorKey; } - /// Specialized version of [ShellRoute], that delegates the building of nested /// [Navigator]s to a child [Widget], created by class NestedNavigationShellRoute extends ShellRoute { - /// Constructs a [NestedNavigationShellRoute]. NestedNavigationShellRoute({ required this.nestedNavigationBuilder, @@ -484,10 +482,11 @@ class NestedNavigationShellRoute extends ShellRoute { /// a nested navigation. final ShellRouteNestedNavigationBuilder nestedNavigationBuilder; - static Page _defaultPageBuilder(BuildContext context, - GoRouterState state, Widget child) { - return NoTransitionPage(child: child, name: state.name ?? state.fullpath, + static Page _defaultPageBuilder( + BuildContext context, GoRouterState state, Widget child) { + return NoTransitionPage( + child: child, + name: state.name ?? state.fullpath, restorationId: state.pageKey.value); } - } From 62574bd48313bfa8e330875feb2fdd2fc7a35d69 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20Lo=CC=88fstrand?= Date: Tue, 27 Sep 2022 10:48:39 +0200 Subject: [PATCH 004/112] Formatting. Added (and fixed) animation when switching tabs. --- .../lib/nested_navigation_shell_route.dart | 153 ++++++++++-------- 1 file changed, 87 insertions(+), 66 deletions(-) diff --git a/packages/go_router/example/lib/nested_navigation_shell_route.dart b/packages/go_router/example/lib/nested_navigation_shell_route.dart index 3a2b29c6a1f6..4509701cdb9a 100644 --- a/packages/go_router/example/lib/nested_navigation_shell_route.dart +++ b/packages/go_router/example/lib/nested_navigation_shell_route.dart @@ -5,7 +5,6 @@ import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; - // This example demonstrates how to setup nested navigation using a // BottomNavigationBar, where each tab uses its own persistent navigator, i.e. // navigation state is maintained separately for each tab. This setup also @@ -20,7 +19,6 @@ void main() { runApp(NestedTabNavigationExampleApp()); } - /// NestedNavigationShellRoute that uses a bottom tab navigation /// (ScaffoldWithNavBar) with separate navigators for each tab. class BottomTabBarShellRoute extends NestedNavigationShellRoute { @@ -30,30 +28,30 @@ class BottomTabBarShellRoute extends NestedNavigationShellRoute { List routes = const [], Key? scaffoldKey = const ValueKey('ScaffoldWithNavBar'), }) : super( - routes: routes, - nestedNavigationBuilder: (BuildContext context, GoRouterState state, - List> pagesForCurrentRoute) { - return ScaffoldWithNavBar(tabs: tabs, key: scaffoldKey, - pagesForCurrentRoute: pagesForCurrentRoute); - } - ); + routes: routes, + nestedNavigationBuilder: (BuildContext context, GoRouterState state, + List> pagesForCurrentRoute) { + return ScaffoldWithNavBar( + tabs: tabs, + key: scaffoldKey, + pagesForCurrentRoute: pagesForCurrentRoute); + }); /// The tabs final List tabs; } - /// An example demonstrating how to use nested navigators class NestedTabNavigationExampleApp extends StatelessWidget { /// Creates a NestedTabNavigationExampleApp NestedTabNavigationExampleApp({Key? key}) : super(key: key); static const List _tabs = - [ - ScaffoldWithNavBarTabItem(initialLocation: '/a', - icon: Icon(Icons.home), label: 'Section A'), - ScaffoldWithNavBarTabItem(initialLocation: '/b', - icon: Icon(Icons.settings), label: 'Section B'), + [ + ScaffoldWithNavBarTabItem( + initialLocation: '/a', icon: Icon(Icons.home), label: 'Section A'), + ScaffoldWithNavBarTabItem( + initialLocation: '/b', icon: Icon(Icons.settings), label: 'Section B'), ]; final GoRouter _router = GoRouter( @@ -70,7 +68,7 @@ class NestedTabNavigationExampleApp extends StatelessWidget { GoRoute( path: '/a', builder: (BuildContext context, GoRouterState state) => - const RootScreen(label: 'A', detailsPath: '/a/details'), + const RootScreen(label: 'A', detailsPath: '/a/details'), routes: [ /// The details screen to display stacked on navigator of the /// first tab. This will cover screen A but not the application @@ -78,16 +76,17 @@ class NestedTabNavigationExampleApp extends StatelessWidget { GoRoute( path: 'details', builder: (BuildContext context, GoRouterState state) => - const DetailsScreen(label: 'A'), + const DetailsScreen(label: 'A'), ), ], ), + /// The screen to display as the root in the second tab of the bottom /// navigation bar. GoRoute( path: '/b', builder: (BuildContext context, GoRouterState state) => - const RootScreen(label: 'B', detailsPath: '/b/details'), + const RootScreen(label: 'B', detailsPath: '/b/details'), routes: [ /// The details screen to display stacked on navigator of the /// second tab. This will cover screen B but not the application @@ -95,7 +94,7 @@ class NestedTabNavigationExampleApp extends StatelessWidget { GoRoute( path: 'details', builder: (BuildContext context, GoRouterState state) => - const DetailsScreen(label: 'B'), + const DetailsScreen(label: 'B'), ), ], ), @@ -118,13 +117,15 @@ class NestedTabNavigationExampleApp extends StatelessWidget { } } - /// Representation of a tab item in a [ScaffoldWithNavBar] class ScaffoldWithNavBarTabItem extends BottomNavigationBarItem { /// Constructs an [ScaffoldWithNavBarTabItem]. - const ScaffoldWithNavBarTabItem({required this.initialLocation, - this.navigatorKey, required Widget icon, String? label}) : - super(icon: icon, label: label); + const ScaffoldWithNavBarTabItem( + {required this.initialLocation, + this.navigatorKey, + required Widget icon, + String? label}) + : super(icon: icon, label: label); /// The initial location/path final String initialLocation; @@ -133,7 +134,6 @@ class ScaffoldWithNavBarTabItem extends BottomNavigationBarItem { final GlobalKey? navigatorKey; } - /// Builds the "shell" for the app by building a Scaffold with a /// BottomNavigationBar, where [child] is placed in the body of the Scaffold. class ScaffoldWithNavBar extends StatefulWidget { @@ -156,23 +156,29 @@ class ScaffoldWithNavBar extends StatefulWidget { } /// State for ScaffoldWithNavBar -class ScaffoldWithNavBarState extends State { - +class ScaffoldWithNavBarState extends State + with SingleTickerProviderStateMixin { + late final AnimationController _animationController; late final List<_NavBarTabNavigator> _tabs; int _locationToTabIndex(String location) { - final int index = _tabs.indexWhere((_NavBarTabNavigator t) => - location.startsWith(t.initialLocation)); + final int index = _tabs.indexWhere( + (_NavBarTabNavigator t) => location.startsWith(t.initialLocation)); return index < 0 ? 0 : index; } - int get _currentIndex => _locationToTabIndex(GoRouter.of(context).location); + int _currentIndex = 0; @override void initState() { super.initState(); - _tabs = widget.tabs.map((ScaffoldWithNavBarTabItem e) => - _NavBarTabNavigator(e)).toList(); + _tabs = widget.tabs + .map((ScaffoldWithNavBarTabItem e) => _NavBarTabNavigator(e)) + .toList(); + + _animationController = AnimationController( + vsync: this, duration: const Duration(milliseconds: 400)); + _animationController.forward(); } @override @@ -181,15 +187,17 @@ class ScaffoldWithNavBarState extends State { final GoRouter route = GoRouter.of(context); final String location = route.location; - final int tabIndex = _locationToTabIndex(location); + final previousIndex = _currentIndex; + _currentIndex = _locationToTabIndex(location); - final _NavBarTabNavigator tabNav = _tabs[tabIndex]; + final _NavBarTabNavigator tabNav = _tabs[_currentIndex]; final List> filteredPages = widget.pagesForCurrentRoute .where((Page p) => p.name!.startsWith(tabNav.initialLocation)) .toList(); if (filteredPages.length == 1 && location != tabNav.initialLocation) { - final int index = tabNav.pages.indexWhere((Page e) => e.name == location); + final int index = + tabNav.pages.indexWhere((Page e) => e.name == location); if (index < 0) { tabNav.pages.add(filteredPages.last); } else { @@ -198,6 +206,16 @@ class ScaffoldWithNavBarState extends State { } else { tabNav.pages = filteredPages; } + + if (previousIndex != _currentIndex) { + _animationController.forward(from: 0.0); + } + } + + @override + void dispose() { + _animationController.dispose(); + super.dispose(); } @override @@ -205,7 +223,9 @@ class ScaffoldWithNavBarState extends State { return Scaffold( body: _buildBody(context), bottomNavigationBar: BottomNavigationBar( - items: _tabs.map((_NavBarTabNavigator e) => e.bottomNavigationTab).toList(), + items: _tabs + .map((_NavBarTabNavigator e) => e.bottomNavigationTab) + .toList(), currentIndex: _currentIndex, onTap: (int idx) => _onItemTapped(idx, context), ), @@ -213,10 +233,13 @@ class ScaffoldWithNavBarState extends State { } Widget _buildBody(BuildContext context) { - return IndexedStack( - index: _currentIndex, - children: _tabs.map((_NavBarTabNavigator tab) => tab.buildNavigator(context)).toList() - ); + return FadeTransition( + opacity: _animationController, + child: IndexedStack( + index: _currentIndex, + children: _tabs + .map((_NavBarTabNavigator tab) => tab.buildNavigator(context)) + .toList())); } void _onItemTapped(int index, BuildContext context) { @@ -224,45 +247,42 @@ class ScaffoldWithNavBarState extends State { } } - -/// Class representing +/// Class representing a tab along with its navigation logic class _NavBarTabNavigator { - _NavBarTabNavigator(this.bottomNavigationTab); - static const String _initialPlaceholderPageName = '#placeholder#'; final ScaffoldWithNavBarTabItem bottomNavigationTab; String get initialLocation => bottomNavigationTab.initialLocation; Key? get navigatorKey => bottomNavigationTab.navigatorKey; List> pages = >[]; - List> get _pagesWithPlaceholder => pages.isNotEmpty ? pages : - >[const MaterialPage(name: _initialPlaceholderPageName, - child: SizedBox.shrink())]; - - String get currentLocation => pages.isNotEmpty ? pages.last.name! : initialLocation; + String get currentLocation => + pages.isNotEmpty ? pages.last.name! : initialLocation; Widget buildNavigator(BuildContext context) { - return Navigator( - key: navigatorKey, - pages: _pagesWithPlaceholder, - onPopPage: (Route route, dynamic result) { - if (pages.length == 1 || !route.didPop(result)) { - return false; - } - GoRouter.of(context).pop(); - return true; - }, - ); + if (pages.isNotEmpty) { + return Navigator( + key: navigatorKey, + pages: pages, + onPopPage: (Route route, dynamic result) { + if (pages.length == 1 || !route.didPop(result)) { + return false; + } + GoRouter.of(context).pop(); + return true; + }, + ); + } else { + return const SizedBox.shrink(); + } } } - /// Widget for the root/initial pages in the bottom navigation bar. class RootScreen extends StatelessWidget { /// Creates a RootScreen - const RootScreen({required this.label, required this.detailsPath, Key? key}) : - super(key: key); + const RootScreen({required this.label, required this.detailsPath, Key? key}) + : super(key: key); /// The label final String label; @@ -280,7 +300,8 @@ class RootScreen extends StatelessWidget { child: Column( mainAxisSize: MainAxisSize.min, children: [ - Text('Screen $label', style: Theme.of(context).textTheme.titleLarge), + Text('Screen $label', + style: Theme.of(context).textTheme.titleLarge), const Padding(padding: EdgeInsets.all(4)), TextButton( onPressed: () { @@ -295,10 +316,8 @@ class RootScreen extends StatelessWidget { } } - /// The details screen for either the A or B screen. class DetailsScreen extends StatefulWidget { - /// Constructs a [DetailsScreen]. const DetailsScreen({ required this.label, @@ -331,7 +350,9 @@ class DetailsScreenState extends State { const Padding(padding: EdgeInsets.all(4)), TextButton( onPressed: () { - setState(() { _counter++; }); + setState(() { + _counter++; + }); }, child: const Text('Increment counter'), ), From 850f41adaa00b20a69d904d2def7dffbda942c90 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20Lo=CC=88fstrand?= Date: Tue, 27 Sep 2022 13:10:41 +0200 Subject: [PATCH 005/112] Removed NestedNavigationShellRoute and replaced it with new field `nestedNavigationBuilder` on ShellRoute. Added unit test. Added reference to example in readme. --- packages/go_router/example/README.md | 6 +++ .../lib/nested_navigation_shell_route.dart | 8 ++-- packages/go_router/lib/go_router.dart | 7 +-- packages/go_router/lib/src/builder.dart | 15 ++++-- packages/go_router/lib/src/delegate.dart | 10 ++-- packages/go_router/lib/src/route.dart | 46 +++++++++---------- packages/go_router/lib/src/typedefs.dart | 3 +- .../go_router/test/configuration_test.dart | 35 ++++++++++++++ 8 files changed, 87 insertions(+), 43 deletions(-) diff --git a/packages/go_router/example/README.md b/packages/go_router/example/README.md index d6ffa67460f8..b9c16a72cbf6 100644 --- a/packages/go_router/example/README.md +++ b/packages/go_router/example/README.md @@ -28,6 +28,12 @@ An example to demonstrate how to use redirect to handle a synchronous sign-in fl ## [Asynchronous Redirection](https://github.com/flutter/packages/blob/main/packages/go_router/example/lib/async_redirection.dart) `flutter run lib/async_redirection.dart` +An example to demonstrate how to use a `Shell Route` to create stateful nested navigation, with a +`BottomNavigationBar`. + +## [Stateful Nested Navigation](https://github.com/flutter/packages/blob/main/packages/go_router/example/lib/nested_navigation_shell_route.dart) +`flutter run lib/sub_routes.dart` + An example to demonstrate how to use handle a sign-in flow with a stream authentication service. ## [Books app](https://github.com/flutter/packages/blob/main/packages/go_router/example/lib/books) diff --git a/packages/go_router/example/lib/nested_navigation_shell_route.dart b/packages/go_router/example/lib/nested_navigation_shell_route.dart index 4509701cdb9a..8f3dd9df7fb1 100644 --- a/packages/go_router/example/lib/nested_navigation_shell_route.dart +++ b/packages/go_router/example/lib/nested_navigation_shell_route.dart @@ -19,9 +19,9 @@ void main() { runApp(NestedTabNavigationExampleApp()); } -/// NestedNavigationShellRoute that uses a bottom tab navigation -/// (ScaffoldWithNavBar) with separate navigators for each tab. -class BottomTabBarShellRoute extends NestedNavigationShellRoute { +/// ShellRoute that uses a bottom tab navigation (ScaffoldWithNavBar) with +/// separate navigators for each tab. +class BottomTabBarShellRoute extends ShellRoute { /// Constructs a BottomTabBarShellRoute BottomTabBarShellRoute({ required this.tabs, @@ -187,7 +187,7 @@ class ScaffoldWithNavBarState extends State final GoRouter route = GoRouter.of(context); final String location = route.location; - final previousIndex = _currentIndex; + final int previousIndex = _currentIndex; _currentIndex = _locationToTabIndex(location); final _NavBarTabNavigator tabNav = _tabs[_currentIndex]; diff --git a/packages/go_router/lib/go_router.dart b/packages/go_router/lib/go_router.dart index fcedbac0a475..1d9f9dd8e15d 100644 --- a/packages/go_router/lib/go_router.dart +++ b/packages/go_router/lib/go_router.dart @@ -7,12 +7,7 @@ library go_router; export 'src/configuration.dart' - show - GoRoute, - GoRouterState, - RouteBase, - ShellRoute, - NestedNavigationShellRoute; + show GoRoute, GoRouterState, RouteBase, ShellRoute; export 'src/misc/extensions.dart'; export 'src/misc/inherited_router.dart'; export 'src/pages/custom_transition_page.dart'; diff --git a/packages/go_router/lib/src/builder.dart b/packages/go_router/lib/src/builder.dart index 24a2e8bae9c0..b0c8728cdf83 100644 --- a/packages/go_router/lib/src/builder.dart +++ b/packages/go_router/lib/src/builder.dart @@ -188,9 +188,12 @@ class RouteBuilder { keyToPages, newParams, shellNavigatorKey); final Widget child; - if (route is NestedNavigationShellRoute) { - // Build the container for the nested routes - child = route.nestedNavigationBuilder( + final ShellRouteNestedNavigationBuilder? nestedNavigationBuilder = + route.nestedNavigationBuilder; + if (nestedNavigationBuilder != null) { + // Build the container for nested routes (delegate responsibility for + // building nested Navigator) + child = nestedNavigationBuilder( context, state, keyToPages[shellNavigatorKey]!); } else { // Build the Navigator @@ -312,6 +315,12 @@ class RouteBuilder { 'Attempt to build ShellRoute without a child widget'); } + // When `nestedNavigationBuilder` is set it supersedes `builder`, and at + // this point it will already have been used to create `childWidget`. + if (route.nestedNavigationBuilder != null) { + return childWidget; + } + final ShellRouteBuilder? builder = route.builder; if (builder == null) { diff --git a/packages/go_router/lib/src/delegate.dart b/packages/go_router/lib/src/delegate.dart index 6ffc24ab4ad8..f4ce2ce48fe0 100644 --- a/packages/go_router/lib/src/delegate.dart +++ b/packages/go_router/lib/src/delegate.dart @@ -121,11 +121,13 @@ class GoRouterDelegate extends RouterDelegate // in this case. assert(canPop); return canPop; - } else if (route is NestedNavigationShellRoute) { - // NestedNavigationShellRoute delegates navigation handling and should - // therefore not handle pop here - continue; } else if (route is ShellRoute) { + if (route.nestedNavigationBuilder != null) { + // When nestedNavigationBuilder is set on a ShellRoute, the navigation + // handling is delegated, meaning ShellRoute doesn't have an associated + // Navigator. + continue; + } final bool canPop = route.navigatorKey.currentState!.canPop(); if (canPop) { return canPop; diff --git a/packages/go_router/lib/src/route.dart b/packages/go_router/lib/src/route.dart index 33e05348026f..41ed7c1e7238 100644 --- a/packages/go_router/lib/src/route.dart +++ b/packages/go_router/lib/src/route.dart @@ -345,6 +345,15 @@ class GoRoute extends RouteBase { /// passed to the /b/details route so that it displays on the root Navigator /// instead of the ShellRoute's Navigator: /// +/// To delegate nested navigation handling entirely to a child of this route, +/// specify [nestedNavigationBuilder] instead of [builder] or [pageBuilder]. +/// Doing so means no [Navigator] will be built by this route. This in +/// convenient when for instance implementing a UI with a [BottomNavigationBar], +/// with a persistent navigation state for each tab (i.e. building a [Navigator] +/// for each tab). +/// See [Stateful Nested Navigation](https://github.com/flutter/packages/blob/main/packages/go_router/example/lib/nested_navigation_shell_route.dart) +/// for a complete runnable example. +/// /// ``` /// final GlobalKey _rootNavigatorKey = /// GlobalKey(); @@ -433,6 +442,7 @@ class ShellRoute extends RouteBase { ShellRoute({ this.builder, this.pageBuilder, + this.nestedNavigationBuilder, super.routes, GlobalKey? navigatorKey, }) : assert(routes.isNotEmpty), @@ -443,6 +453,10 @@ class ShellRoute extends RouteBase { assert(route.parentNavigatorKey == null); } } + if (nestedNavigationBuilder != null) { + assert(builder == null, + 'Cannot use both builder and nestedNavigationBuilder'); + } } /// The widget builder for a shell route. @@ -459,34 +473,16 @@ class ShellRoute extends RouteBase { /// sub-route's builder. final ShellRoutePageBuilder? pageBuilder; - /// The [GlobalKey] to be used by the [Navigator] built for this route. - /// All ShellRoutes build a Navigator by default. Child GoRoutes - /// are placed onto this Navigator instead of the root Navigator. - final GlobalKey navigatorKey; -} - -/// Specialized version of [ShellRoute], that delegates the building of nested -/// [Navigator]s to a child [Widget], created by -class NestedNavigationShellRoute extends ShellRoute { - /// Constructs a [NestedNavigationShellRoute]. - NestedNavigationShellRoute({ - required this.nestedNavigationBuilder, - required super.routes, - ShellRoutePageBuilder? pageBuilder, - }) : super(pageBuilder: pageBuilder ?? _defaultPageBuilder); - /// The widget builder for a nested navigation shell route. /// /// Similar to GoRoute builder, but with an additional pages parameter. This /// pages parameter contains the pages that should currently be displayed in - /// a nested navigation. - final ShellRouteNestedNavigationBuilder nestedNavigationBuilder; + /// a nested navigation. This nested navigation builder is a replacement for + /// [builder] (i.e. both cannot be used as the same time). + final ShellRouteNestedNavigationBuilder? nestedNavigationBuilder; - static Page _defaultPageBuilder( - BuildContext context, GoRouterState state, Widget child) { - return NoTransitionPage( - child: child, - name: state.name ?? state.fullpath, - restorationId: state.pageKey.value); - } + /// The [GlobalKey] to be used by the [Navigator] built for this route. + /// All ShellRoutes build a Navigator by default. Child GoRoutes + /// are placed onto this Navigator instead of the root Navigator. + final GlobalKey navigatorKey; } diff --git a/packages/go_router/lib/src/typedefs.dart b/packages/go_router/lib/src/typedefs.dart index a6bef39d2eaf..85c6db3a77e4 100644 --- a/packages/go_router/lib/src/typedefs.dart +++ b/packages/go_router/lib/src/typedefs.dart @@ -34,7 +34,8 @@ typedef ShellRoutePageBuilder = Page Function( Widget child, ); -/// The builder for a [NestedNavigationShellRoute] +/// A builder used for customizing the creation of nested navigation in a +/// [ShellRoute]. typedef ShellRouteNestedNavigationBuilder = Widget Function( BuildContext context, GoRouterState state, diff --git a/packages/go_router/test/configuration_test.dart b/packages/go_router/test/configuration_test.dart index 61f769f4c539..f306029a530a 100644 --- a/packages/go_router/test/configuration_test.dart +++ b/packages/go_router/test/configuration_test.dart @@ -80,6 +80,37 @@ void main() { ); }); + test( + 'throws when ShellRoute has both a builder and a nestedNavigationBuilder', + () { + final GlobalKey root = + GlobalKey(debugLabel: 'root'); + final List shellRouteChildren = [ + GoRoute( + path: '/a', + builder: _mockScreenBuilder, + ) + ]; + expect( + () { + RouteConfiguration( + navigatorKey: root, + routes: [ + ShellRoute( + routes: shellRouteChildren, + builder: _mockShellBuilder, + nestedNavigationBuilder: _mockShellNavigationBuilder), + ], + redirectLimit: 10, + topRedirect: (BuildContext context, GoRouterState state) { + return null; + }, + ); + }, + throwsAssertionError, + ); + }); + test( 'throws when there is a GoRoute ancestor with a different parentNavigatorKey', () { @@ -469,3 +500,7 @@ Widget _mockScreenBuilder(BuildContext context, GoRouterState state) => Widget _mockShellBuilder( BuildContext context, GoRouterState state, Widget child) => child; + +Widget _mockShellNavigationBuilder( + BuildContext context, GoRouterState state, List> pages) => + _MockScreen(key: state.pageKey); From 1c0458851218d4ed201f827c8e713b7b7e555d65 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20Lo=CC=88fstrand?= Date: Tue, 27 Sep 2022 14:27:32 +0200 Subject: [PATCH 006/112] Rebased onto upstream/main. Conflicts: packages/go_router/CHANGELOG.md packages/go_router/pubspec.yaml --- packages/go_router/CHANGELOG.md | 5 +++++ packages/go_router/pubspec.yaml | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/go_router/CHANGELOG.md b/packages/go_router/CHANGELOG.md index 5d6e28f6993c..8fb45727ab21 100644 --- a/packages/go_router/CHANGELOG.md +++ b/packages/go_router/CHANGELOG.md @@ -1,3 +1,8 @@ +## 5.1.0 + +- Adds support for customizing the nested navigation of ShellRoute, to support things like + preserving state in nested navigators (flutter/flutter#99124). + ## 5.0.3 - Changes examples to use the routerConfig API diff --git a/packages/go_router/pubspec.yaml b/packages/go_router/pubspec.yaml index 3f45de022af7..e0a7c08a8560 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: 5.0.3 +version: 5.1.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 From 1db6bba5471cd60a56f6e2e39b10bf9787221a5d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20Lo=CC=88fstrand?= Date: Tue, 27 Sep 2022 14:47:17 +0200 Subject: [PATCH 007/112] Fixed typos. --- packages/go_router/CHANGELOG.md | 2 +- packages/go_router/example/README.md | 2 +- packages/go_router/lib/src/route.dart | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/go_router/CHANGELOG.md b/packages/go_router/CHANGELOG.md index 8fb45727ab21..10d7f7f70fb0 100644 --- a/packages/go_router/CHANGELOG.md +++ b/packages/go_router/CHANGELOG.md @@ -1,6 +1,6 @@ ## 5.1.0 -- Adds support for customizing the nested navigation of ShellRoute, to support things like +- Adds support for customising the nested navigation of `ShellRoute`, to support things like preserving state in nested navigators (flutter/flutter#99124). ## 5.0.3 diff --git a/packages/go_router/example/README.md b/packages/go_router/example/README.md index b9c16a72cbf6..47ecd7d74b58 100644 --- a/packages/go_router/example/README.md +++ b/packages/go_router/example/README.md @@ -32,7 +32,7 @@ An example to demonstrate how to use a `Shell Route` to create stateful nested n `BottomNavigationBar`. ## [Stateful Nested Navigation](https://github.com/flutter/packages/blob/main/packages/go_router/example/lib/nested_navigation_shell_route.dart) -`flutter run lib/sub_routes.dart` +`flutter run lib/nested_navigation_shell_route.dart` An example to demonstrate how to use handle a sign-in flow with a stream authentication service. diff --git a/packages/go_router/lib/src/route.dart b/packages/go_router/lib/src/route.dart index 41ed7c1e7238..d810aef555e9 100644 --- a/packages/go_router/lib/src/route.dart +++ b/packages/go_router/lib/src/route.dart @@ -347,7 +347,7 @@ class GoRoute extends RouteBase { /// /// To delegate nested navigation handling entirely to a child of this route, /// specify [nestedNavigationBuilder] instead of [builder] or [pageBuilder]. -/// Doing so means no [Navigator] will be built by this route. This in +/// Doing so means no [Navigator] will be built by this route. This is /// convenient when for instance implementing a UI with a [BottomNavigationBar], /// with a persistent navigation state for each tab (i.e. building a [Navigator] /// for each tab). From 6028c9068ed93d3880e47a6b5c97939dd7a66553 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20Lo=CC=88fstrand?= Date: Wed, 28 Sep 2022 12:22:28 +0200 Subject: [PATCH 008/112] Updated documentation of property navigatorKey on ShellRoute. --- packages/go_router/lib/src/route.dart | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/packages/go_router/lib/src/route.dart b/packages/go_router/lib/src/route.dart index d810aef555e9..e948a5f78c43 100644 --- a/packages/go_router/lib/src/route.dart +++ b/packages/go_router/lib/src/route.dart @@ -482,7 +482,12 @@ class ShellRoute extends RouteBase { final ShellRouteNestedNavigationBuilder? nestedNavigationBuilder; /// The [GlobalKey] to be used by the [Navigator] built for this route. - /// All ShellRoutes build a Navigator by default. Child GoRoutes - /// are placed onto this Navigator instead of the root Navigator. + /// + /// Unless [nestedNavigationBuilder] is set, all ShellRoutes build a Navigator + /// by default. Child GoRoutes are placed onto this Navigator instead of the + /// root Navigator. However, if [nestedNavigationBuilder] is set, construction + /// of a nested Navigator is instead delegated to the widget created by that + /// builder. In that scenario, this property will not be associated with a + /// Navigator, and should not be used to access NavigatorState. final GlobalKey navigatorKey; } From 5f2e995d5f272bbd781792289ea154d059deea17 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20Lo=CC=88fstrand?= Date: Wed, 28 Sep 2022 12:22:56 +0200 Subject: [PATCH 009/112] Added another unit test for ShellRoute with nestedNavigationBuilder. --- packages/go_router/test/builder_test.dart | 62 +++++++++++++++++++++++ 1 file changed, 62 insertions(+) diff --git a/packages/go_router/test/builder_test.dart b/packages/go_router/test/builder_test.dart index 92eb17fb0b75..0c0fbc5ccbbb 100644 --- a/packages/go_router/test/builder_test.dart +++ b/packages/go_router/test/builder_test.dart @@ -98,6 +98,68 @@ void main() { expect(find.byType(_DetailsScreen), findsOneWidget); }); + testWidgets('Builds ShellRoute with nestedNavigationBuilder', + (WidgetTester tester) async { + final RouteConfiguration config = RouteConfiguration( + routes: [ + ShellRoute( + nestedNavigationBuilder: (BuildContext context, + GoRouterState state, List> pages) { + return Navigator( + pages: pages, + onPopPage: (Route route, dynamic result) { + return false; + }); + }, + routes: [ + GoRoute( + path: '/nested', + builder: (BuildContext context, GoRouterState state) { + return _DetailsScreen(); + }, + ), + ]), + ], + redirectLimit: 10, + topRedirect: (BuildContext context, GoRouterState state) { + return null; + }, + navigatorKey: GlobalKey(), + ); + + final RouteMatchList matches = RouteMatchList([ + RouteMatch( + route: config.routes.first, + subloc: '', + fullpath: '', + encodedParams: {}, + queryParams: {}, + queryParametersAll: >{}, + extra: null, + error: null, + ), + RouteMatch( + route: config.routes.first.routes.first, + subloc: '/nested', + fullpath: '/nested', + encodedParams: {}, + queryParams: {}, + queryParametersAll: >{}, + extra: null, + error: null, + ), + ]); + + await tester.pumpWidget( + _BuilderTestWidget( + routeConfiguration: config, + matches: matches, + ), + ); + + expect(find.byType(_DetailsScreen), findsOneWidget); + }); + testWidgets('Uses the correct navigatorKey', (WidgetTester tester) async { final GlobalKey rootNavigatorKey = GlobalKey(); From 461efd999aa394789a1e86d8b9ffb08894fb6716 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20Lo=CC=88fstrand?= Date: Wed, 28 Sep 2022 12:27:49 +0200 Subject: [PATCH 010/112] Updated example to use nested ShellRoutes instead of creating nested navigators manually. Renamed example file to stateful_nested_navigation.dart. --- packages/go_router/example/README.md | 4 +- ...e.dart => stateful_nested_navigation.dart} | 196 +++++++++++------- packages/go_router/lib/src/route.dart | 2 +- 3 files changed, 120 insertions(+), 82 deletions(-) rename packages/go_router/example/lib/{nested_navigation_shell_route.dart => stateful_nested_navigation.dart} (62%) diff --git a/packages/go_router/example/README.md b/packages/go_router/example/README.md index 47ecd7d74b58..c84bc719fb88 100644 --- a/packages/go_router/example/README.md +++ b/packages/go_router/example/README.md @@ -31,8 +31,8 @@ An example to demonstrate how to use redirect to handle a synchronous sign-in fl An example to demonstrate how to use a `Shell Route` to create stateful nested navigation, with a `BottomNavigationBar`. -## [Stateful Nested Navigation](https://github.com/flutter/packages/blob/main/packages/go_router/example/lib/nested_navigation_shell_route.dart) -`flutter run lib/nested_navigation_shell_route.dart` +## [Stateful Nested Navigation](https://github.com/flutter/packages/blob/main/packages/go_router/example/lib/stateful_nested_navigation.dart) +`flutter run lib/stateful_nested_navigation.dart` An example to demonstrate how to use handle a sign-in flow with a stream authentication service. diff --git a/packages/go_router/example/lib/nested_navigation_shell_route.dart b/packages/go_router/example/lib/stateful_nested_navigation.dart similarity index 62% rename from packages/go_router/example/lib/nested_navigation_shell_route.dart rename to packages/go_router/example/lib/stateful_nested_navigation.dart index 8f3dd9df7fb1..38cf0284ae5f 100644 --- a/packages/go_router/example/lib/nested_navigation_shell_route.dart +++ b/packages/go_router/example/lib/stateful_nested_navigation.dart @@ -2,39 +2,71 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; +final GlobalKey _sectionANavigatorKey = + GlobalKey(debugLabel: 'sectionANav'); +final GlobalKey _sectionBNavigatorKey = + GlobalKey(debugLabel: 'sectionBNav'); + // This example demonstrates how to setup nested navigation using a // BottomNavigationBar, where each tab uses its own persistent navigator, i.e. // navigation state is maintained separately for each tab. This setup also // enables deep linking into nested pages. // -// The example is loosely based on the ShellRoute sample in go_router, but -// differs in that it is able to maintain the navigation state of each tab. -// This example introduces a fex (imperfect) classes that ideally should be part -// of go_router, such as BottomTabBarShellRoute etc. +// This example demonstrates how to display routes within a ShellRoute using a +// `nestedNavigationBuilder`. Navigators for the tabs ('Section A' and +// 'Section B') are created via nested ShellRoutes. Note that no navigator will +// be created by the "top" ShellRoute. This example is similar to the ShellRoute +// example, but differs in that it is able to maintain the navigation state of +// each tab. void main() { runApp(NestedTabNavigationExampleApp()); } +/// Extension to get the child widget of the Page. Wish there were a better way... +extension PageWithChild on Page { + /// Gets the child of the page + Widget? findTheChild() { + final Page widget = this; + if (widget is MaterialPage) { + return widget.child; + } else if (widget is CupertinoPage) { + return widget.child; + } else if (widget is CustomTransitionPage) { + return widget.child; + } + return null; + // An unsafer option... : + //return (this as dynamic).child as Widget; + } +} + /// ShellRoute that uses a bottom tab navigation (ScaffoldWithNavBar) with /// separate navigators for each tab. class BottomTabBarShellRoute extends ShellRoute { /// Constructs a BottomTabBarShellRoute BottomTabBarShellRoute({ required this.tabs, + GlobalKey? navigatorKey, List routes = const [], Key? scaffoldKey = const ValueKey('ScaffoldWithNavBar'), }) : super( + navigatorKey: navigatorKey, routes: routes, nestedNavigationBuilder: (BuildContext context, GoRouterState state, List> pagesForCurrentRoute) { + // The first (and only) page will be the nested navigator for the + // current tab. The pages parameter will in this case + final Widget? shellNav = + pagesForCurrentRoute.first.findTheChild(); return ScaffoldWithNavBar( tabs: tabs, key: scaffoldKey, - pagesForCurrentRoute: pagesForCurrentRoute); + shellNav: shellNav! as Navigator); }); /// The tabs @@ -46,55 +78,77 @@ class NestedTabNavigationExampleApp extends StatelessWidget { /// Creates a NestedTabNavigationExampleApp NestedTabNavigationExampleApp({Key? key}) : super(key: key); - static const List _tabs = + static final List _tabs = [ ScaffoldWithNavBarTabItem( - initialLocation: '/a', icon: Icon(Icons.home), label: 'Section A'), + initialLocation: '/a', + navigatorKey: _sectionANavigatorKey, + icon: const Icon(Icons.home), + label: 'Section A'), ScaffoldWithNavBarTabItem( - initialLocation: '/b', icon: Icon(Icons.settings), label: 'Section B'), + initialLocation: '/b', + navigatorKey: _sectionBNavigatorKey, + icon: const Icon(Icons.settings), + label: 'Section B', + ), ]; final GoRouter _router = GoRouter( initialLocation: '/a', - navigatorKey: GlobalKey(debugLabel: 'Root'), routes: [ - /// Custom shell route - wraps the below routes in a scaffold with - /// a bottom tab navigator + /// Custom top shell route - wraps the below routes in a scaffold with + /// a bottom tab navigator (ScaffoldWithNavBar). Note that no Navigator + /// will be created by this top ShellRoute. BottomTabBarShellRoute( tabs: _tabs, routes: [ - /// The screen to display as the root in the first tab of the bottom - /// navigation bar. - GoRoute( - path: '/a', - builder: (BuildContext context, GoRouterState state) => - const RootScreen(label: 'A', detailsPath: '/a/details'), - routes: [ - /// The details screen to display stacked on navigator of the - /// first tab. This will cover screen A but not the application - /// shell (bottom navigation bar). - GoRoute( - path: 'details', - builder: (BuildContext context, GoRouterState state) => - const DetailsScreen(label: 'A'), - ), - ], - ), - - /// The screen to display as the root in the second tab of the bottom - /// navigation bar. - GoRoute( - path: '/b', - builder: (BuildContext context, GoRouterState state) => - const RootScreen(label: 'B', detailsPath: '/b/details'), + ShellRoute( + navigatorKey: _sectionANavigatorKey, + builder: + (BuildContext context, GoRouterState state, Widget child) { + return child; + }, + routes: [ + /// The screen to display as the root in the first tab of the bottom + /// navigation bar. + GoRoute( + path: '/a', + builder: (BuildContext context, GoRouterState state) => + const RootScreen(label: 'A', detailsPath: '/a/details'), + routes: [ + /// The details screen to display stacked on navigator of the + /// first tab. This will cover screen A but not the application + /// shell (bottom navigation bar). + GoRoute( + path: 'details', + builder: (BuildContext context, GoRouterState state) => + const DetailsScreen(label: 'A'), + ), + ], + ), + ]), + ShellRoute( + navigatorKey: _sectionBNavigatorKey, + builder: (BuildContext context, GoRouterState state, Widget child) { + return child; + }, routes: [ - /// The details screen to display stacked on navigator of the - /// second tab. This will cover screen B but not the application - /// shell (bottom navigation bar). + /// The screen to display as the root in the second tab of the bottom + /// navigation bar. GoRoute( - path: 'details', + path: '/b', builder: (BuildContext context, GoRouterState state) => - const DetailsScreen(label: 'B'), + const RootScreen(label: 'B', detailsPath: '/b/details'), + routes: [ + /// The details screen to display stacked on navigator of the + /// second tab. This will cover screen B but not the application + /// shell (bottom navigation bar). + GoRoute( + path: 'details', + builder: (BuildContext context, GoRouterState state) => + const DetailsScreen(label: 'B'), + ), + ], ), ], ), @@ -122,7 +176,7 @@ class ScaffoldWithNavBarTabItem extends BottomNavigationBarItem { /// Constructs an [ScaffoldWithNavBarTabItem]. const ScaffoldWithNavBarTabItem( {required this.initialLocation, - this.navigatorKey, + required this.navigatorKey, required Widget icon, String? label}) : super(icon: icon, label: label); @@ -131,7 +185,7 @@ class ScaffoldWithNavBarTabItem extends BottomNavigationBarItem { final String initialLocation; /// Optional navigatorKey - final GlobalKey? navigatorKey; + final GlobalKey navigatorKey; } /// Builds the "shell" for the app by building a Scaffold with a @@ -139,14 +193,13 @@ class ScaffoldWithNavBarTabItem extends BottomNavigationBarItem { class ScaffoldWithNavBar extends StatefulWidget { /// Constructs an [ScaffoldWithNavBar]. const ScaffoldWithNavBar({ - required this.pagesForCurrentRoute, + required this.shellNav, required this.tabs, Key? key, }) : super(key: key ?? const ValueKey('ScaffoldWithNavBar')); - /// The widget to display in the body of the Scaffold. - /// In this sample, it is a Navigator. - final List> pagesForCurrentRoute; + /// The navigator for the currently active tab + final Navigator shellNav; /// The tabs final List tabs; @@ -184,28 +237,22 @@ class ScaffoldWithNavBarState extends State @override void didUpdateWidget(covariant ScaffoldWithNavBar oldWidget) { super.didUpdateWidget(oldWidget); - final GoRouter route = GoRouter.of(context); - final String location = route.location; + _updateForCurrentTab(); + } + + @override + void didChangeDependencies() { + super.didChangeDependencies(); + _updateForCurrentTab(); + } + void _updateForCurrentTab() { final int previousIndex = _currentIndex; - _currentIndex = _locationToTabIndex(location); + _currentIndex = _locationToTabIndex(GoRouter.of(context).location); final _NavBarTabNavigator tabNav = _tabs[_currentIndex]; - final List> filteredPages = widget.pagesForCurrentRoute - .where((Page p) => p.name!.startsWith(tabNav.initialLocation)) - .toList(); - - if (filteredPages.length == 1 && location != tabNav.initialLocation) { - final int index = - tabNav.pages.indexWhere((Page e) => e.name == location); - if (index < 0) { - tabNav.pages.add(filteredPages.last); - } else { - tabNav.pages.length = index + 1; - } - } else { - tabNav.pages = filteredPages; - } + tabNav.navigator = widget.shellNav; + assert(widget.shellNav.key == tabNav.navigatorKey); if (previousIndex != _currentIndex) { _animationController.forward(from: 0.0); @@ -252,26 +299,17 @@ class _NavBarTabNavigator { _NavBarTabNavigator(this.bottomNavigationTab); final ScaffoldWithNavBarTabItem bottomNavigationTab; - String get initialLocation => bottomNavigationTab.initialLocation; - Key? get navigatorKey => bottomNavigationTab.navigatorKey; - List> pages = >[]; + Navigator? navigator; + String get initialLocation => bottomNavigationTab.initialLocation; + Key get navigatorKey => bottomNavigationTab.navigatorKey; + List> get pages => navigator?.pages ?? >[]; String get currentLocation => pages.isNotEmpty ? pages.last.name! : initialLocation; Widget buildNavigator(BuildContext context) { - if (pages.isNotEmpty) { - return Navigator( - key: navigatorKey, - pages: pages, - onPopPage: (Route route, dynamic result) { - if (pages.length == 1 || !route.didPop(result)) { - return false; - } - GoRouter.of(context).pop(); - return true; - }, - ); + if (navigator != null) { + return navigator!; } else { return const SizedBox.shrink(); } diff --git a/packages/go_router/lib/src/route.dart b/packages/go_router/lib/src/route.dart index e948a5f78c43..999b1010b8f9 100644 --- a/packages/go_router/lib/src/route.dart +++ b/packages/go_router/lib/src/route.dart @@ -351,7 +351,7 @@ class GoRoute extends RouteBase { /// convenient when for instance implementing a UI with a [BottomNavigationBar], /// with a persistent navigation state for each tab (i.e. building a [Navigator] /// for each tab). -/// See [Stateful Nested Navigation](https://github.com/flutter/packages/blob/main/packages/go_router/example/lib/nested_navigation_shell_route.dart) +/// See [Stateful Nested Navigation](https://github.com/flutter/packages/blob/main/packages/go_router/example/lib/stateful_nested_navigation.dart) /// for a complete runnable example. /// /// ``` From 0315e1a37cc66ae2fc1d70c3b62f6d1893d48c6b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20Lo=CC=88fstrand?= Date: Fri, 30 Sep 2022 05:10:20 +0200 Subject: [PATCH 011/112] Refactored support for nested stateful navigation - introduced new route class PartitionedShellRoute as well new base class (ShellRouteBase) shared with ShellRoute. Introduced new widget (StackedNavigationScaffold) for building stacked, stateful navigation based on an IndexStack, as well as factory constructor (stackedNavigation) for this on PartitionedShellRoute. --- .../lib/stateful_nested_navigation.dart | 323 ++++++------------ packages/go_router/lib/go_router.dart | 3 +- packages/go_router/lib/src/builder.dart | 57 ++-- packages/go_router/lib/src/configuration.dart | 12 +- packages/go_router/lib/src/delegate.dart | 8 +- packages/go_router/lib/src/match.dart | 2 +- packages/go_router/lib/src/matching.dart | 6 +- .../src/misc/stacked_navigation_scaffold.dart | 207 +++++++++++ packages/go_router/lib/src/route.dart | 133 +++++--- packages/go_router/lib/src/typedefs.dart | 8 - packages/go_router/test/builder_test.dart | 18 +- .../go_router/test/configuration_test.dart | 35 -- 12 files changed, 459 insertions(+), 353 deletions(-) create mode 100644 packages/go_router/lib/src/misc/stacked_navigation_scaffold.dart diff --git a/packages/go_router/example/lib/stateful_nested_navigation.dart b/packages/go_router/example/lib/stateful_nested_navigation.dart index 38cf0284ae5f..a45408188454 100644 --- a/packages/go_router/example/lib/stateful_nested_navigation.dart +++ b/packages/go_router/example/lib/stateful_nested_navigation.dart @@ -2,7 +2,6 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; @@ -27,52 +26,6 @@ void main() { runApp(NestedTabNavigationExampleApp()); } -/// Extension to get the child widget of the Page. Wish there were a better way... -extension PageWithChild on Page { - /// Gets the child of the page - Widget? findTheChild() { - final Page widget = this; - if (widget is MaterialPage) { - return widget.child; - } else if (widget is CupertinoPage) { - return widget.child; - } else if (widget is CustomTransitionPage) { - return widget.child; - } - return null; - // An unsafer option... : - //return (this as dynamic).child as Widget; - } -} - -/// ShellRoute that uses a bottom tab navigation (ScaffoldWithNavBar) with -/// separate navigators for each tab. -class BottomTabBarShellRoute extends ShellRoute { - /// Constructs a BottomTabBarShellRoute - BottomTabBarShellRoute({ - required this.tabs, - GlobalKey? navigatorKey, - List routes = const [], - Key? scaffoldKey = const ValueKey('ScaffoldWithNavBar'), - }) : super( - navigatorKey: navigatorKey, - routes: routes, - nestedNavigationBuilder: (BuildContext context, GoRouterState state, - List> pagesForCurrentRoute) { - // The first (and only) page will be the nested navigator for the - // current tab. The pages parameter will in this case - final Widget? shellNav = - pagesForCurrentRoute.first.findTheChild(); - return ScaffoldWithNavBar( - tabs: tabs, - key: scaffoldKey, - shellNav: shellNav! as Navigator); - }); - - /// The tabs - final List tabs; -} - /// An example demonstrating how to use nested navigators class NestedTabNavigationExampleApp extends StatelessWidget { /// Creates a NestedTabNavigationExampleApp @@ -81,13 +34,13 @@ class NestedTabNavigationExampleApp extends StatelessWidget { static final List _tabs = [ ScaffoldWithNavBarTabItem( - initialLocation: '/a', - navigatorKey: _sectionANavigatorKey, + navigationItem: StackedNavigationItem( + initialLocation: '/a', navigatorKey: _sectionANavigatorKey), icon: const Icon(Icons.home), label: 'Section A'), ScaffoldWithNavBarTabItem( - initialLocation: '/b', - navigatorKey: _sectionBNavigatorKey, + navigationItem: StackedNavigationItem( + initialLocation: '/b', navigatorKey: _sectionBNavigatorKey), icon: const Icon(Icons.settings), label: 'Section B', ), @@ -97,58 +50,65 @@ class NestedTabNavigationExampleApp extends StatelessWidget { initialLocation: '/a', routes: [ /// Custom top shell route - wraps the below routes in a scaffold with - /// a bottom tab navigator (ScaffoldWithNavBar). Note that no Navigator - /// will be created by this top ShellRoute. - BottomTabBarShellRoute( - tabs: _tabs, - routes: [ - ShellRoute( - navigatorKey: _sectionANavigatorKey, - builder: - (BuildContext context, GoRouterState state, Widget child) { - return child; - }, - routes: [ - /// The screen to display as the root in the first tab of the bottom - /// navigation bar. - GoRoute( - path: '/a', - builder: (BuildContext context, GoRouterState state) => - const RootScreen(label: 'A', detailsPath: '/a/details'), - routes: [ - /// The details screen to display stacked on navigator of the - /// first tab. This will cover screen A but not the application - /// shell (bottom navigation bar). - GoRoute( - path: 'details', - builder: (BuildContext context, GoRouterState state) => - const DetailsScreen(label: 'A'), - ), - ], - ), - ]), - ShellRoute( - navigatorKey: _sectionBNavigatorKey, - builder: (BuildContext context, GoRouterState state, Widget child) { - return child; - }, + /// a bottom tab navigator (ScaffoldWithNavBar). Each tab will use its own + /// Navigator, provided by MultiPathShellRoute. + PartitionedShellRoute.stackedNavigation( + stackItems: _tabs + .map((ScaffoldWithNavBarTabItem e) => e.navigationItem) + .toList(), + scaffoldBuilder: (BuildContext context, int currentIndex, + List itemsState, Widget scaffoldBody) { + return ScaffoldWithNavBar( + tabs: _tabs, + currentIndex: currentIndex, + itemsState: itemsState, + body: scaffoldBody); + }, + + /// A transition builder is optional, only included here for + /// demonstration purposes. + transitionBuilder: + (BuildContext context, Animation animation, Widget child) => + FadeTransition(opacity: animation, child: child), + routes: [ + /// The screen to display as the root in the first tab of the bottom + /// navigation bar. Note that the root route must specify the + /// `parentNavigatorKey` + GoRoute( + parentNavigatorKey: _sectionANavigatorKey, + path: '/a', + builder: (BuildContext context, GoRouterState state) => + const RootScreen(label: 'A', detailsPath: '/a/details'), routes: [ - /// The screen to display as the root in the second tab of the bottom - /// navigation bar. + /// The details screen to display stacked on navigator of the + /// first tab. This will cover screen A but not the application + /// shell (bottom navigation bar). GoRoute( - path: '/b', + path: 'details', builder: (BuildContext context, GoRouterState state) => - const RootScreen(label: 'B', detailsPath: '/b/details'), - routes: [ - /// The details screen to display stacked on navigator of the - /// second tab. This will cover screen B but not the application - /// shell (bottom navigation bar). - GoRoute( - path: 'details', - builder: (BuildContext context, GoRouterState state) => - const DetailsScreen(label: 'B'), - ), - ], + const DetailsScreen(label: 'A'), + ), + ], + ), + + /// The screen to display as the root in the second tab of the bottom + /// navigation bar. + GoRoute( + parentNavigatorKey: _sectionBNavigatorKey, + path: '/b', + builder: (BuildContext context, GoRouterState state) => + const RootScreen( + label: 'B', + detailsPath: '/b/details/1', + detailsPath2: '/b/details/2'), + routes: [ + /// The details screen to display stacked on navigator of the + /// second tab. This will cover screen B but not the application + /// shell (bottom navigation bar). + GoRoute( + path: 'details/:param', + builder: (BuildContext context, GoRouterState state) => + DetailsScreen(label: 'B', param: state.params['param']), ), ], ), @@ -175,151 +135,67 @@ class NestedTabNavigationExampleApp extends StatelessWidget { class ScaffoldWithNavBarTabItem extends BottomNavigationBarItem { /// Constructs an [ScaffoldWithNavBarTabItem]. const ScaffoldWithNavBarTabItem( - {required this.initialLocation, - required this.navigatorKey, - required Widget icon, - String? label}) + {required this.navigationItem, required Widget icon, String? label}) : super(icon: icon, label: label); - /// The initial location/path - final String initialLocation; + /// The [StackedNavigationItem] + final StackedNavigationItem navigationItem; - /// Optional navigatorKey - final GlobalKey navigatorKey; + /// Gets the associated navigator key + GlobalKey get navigatorKey => navigationItem.navigatorKey; } /// Builds the "shell" for the app by building a Scaffold with a /// BottomNavigationBar, where [child] is placed in the body of the Scaffold. -class ScaffoldWithNavBar extends StatefulWidget { +class ScaffoldWithNavBar extends StatelessWidget { /// Constructs an [ScaffoldWithNavBar]. const ScaffoldWithNavBar({ - required this.shellNav, + required this.currentIndex, + required this.itemsState, + required this.body, required this.tabs, Key? key, }) : super(key: key ?? const ValueKey('ScaffoldWithNavBar')); - /// The navigator for the currently active tab - final Navigator shellNav; - - /// The tabs - final List tabs; - - @override - State createState() => ScaffoldWithNavBarState(); -} - -/// State for ScaffoldWithNavBar -class ScaffoldWithNavBarState extends State - with SingleTickerProviderStateMixin { - late final AnimationController _animationController; - late final List<_NavBarTabNavigator> _tabs; - - int _locationToTabIndex(String location) { - final int index = _tabs.indexWhere( - (_NavBarTabNavigator t) => location.startsWith(t.initialLocation)); - return index < 0 ? 0 : index; - } - - int _currentIndex = 0; - - @override - void initState() { - super.initState(); - _tabs = widget.tabs - .map((ScaffoldWithNavBarTabItem e) => _NavBarTabNavigator(e)) - .toList(); - - _animationController = AnimationController( - vsync: this, duration: const Duration(milliseconds: 400)); - _animationController.forward(); - } - - @override - void didUpdateWidget(covariant ScaffoldWithNavBar oldWidget) { - super.didUpdateWidget(oldWidget); - _updateForCurrentTab(); - } + /// Currently active tab index + final int currentIndex; - @override - void didChangeDependencies() { - super.didChangeDependencies(); - _updateForCurrentTab(); - } + /// Route state + final List itemsState; - void _updateForCurrentTab() { - final int previousIndex = _currentIndex; - _currentIndex = _locationToTabIndex(GoRouter.of(context).location); + /// Body, i.e. the index stack + final Widget body; - final _NavBarTabNavigator tabNav = _tabs[_currentIndex]; - tabNav.navigator = widget.shellNav; - assert(widget.shellNav.key == tabNav.navigatorKey); - - if (previousIndex != _currentIndex) { - _animationController.forward(from: 0.0); - } - } - - @override - void dispose() { - _animationController.dispose(); - super.dispose(); - } + /// The tabs + final List tabs; @override Widget build(BuildContext context) { return Scaffold( - body: _buildBody(context), + body: body, bottomNavigationBar: BottomNavigationBar( - items: _tabs - .map((_NavBarTabNavigator e) => e.bottomNavigationTab) - .toList(), - currentIndex: _currentIndex, - onTap: (int idx) => _onItemTapped(idx, context), + items: tabs, + currentIndex: currentIndex, + onTap: (int tappedIndex) => + _onItemTapped(context, itemsState[tappedIndex]), ), ); } - Widget _buildBody(BuildContext context) { - return FadeTransition( - opacity: _animationController, - child: IndexedStack( - index: _currentIndex, - children: _tabs - .map((_NavBarTabNavigator tab) => tab.buildNavigator(context)) - .toList())); - } - - void _onItemTapped(int index, BuildContext context) { - GoRouter.of(context).go(_tabs[index].currentLocation); - } -} - -/// Class representing a tab along with its navigation logic -class _NavBarTabNavigator { - _NavBarTabNavigator(this.bottomNavigationTab); - - final ScaffoldWithNavBarTabItem bottomNavigationTab; - Navigator? navigator; - - String get initialLocation => bottomNavigationTab.initialLocation; - Key get navigatorKey => bottomNavigationTab.navigatorKey; - List> get pages => navigator?.pages ?? >[]; - String get currentLocation => - pages.isNotEmpty ? pages.last.name! : initialLocation; - - Widget buildNavigator(BuildContext context) { - if (navigator != null) { - return navigator!; - } else { - return const SizedBox.shrink(); - } + void _onItemTapped( + BuildContext context, StackedNavigationItemState itemState) { + GoRouter.of(context).go(itemState.currentLocation); } } /// Widget for the root/initial pages in the bottom navigation bar. class RootScreen extends StatelessWidget { /// Creates a RootScreen - const RootScreen({required this.label, required this.detailsPath, Key? key}) + const RootScreen( + {required this.label, + required this.detailsPath, + this.detailsPath2, + Key? key}) : super(key: key); /// The label @@ -328,6 +204,9 @@ class RootScreen extends StatelessWidget { /// The path to the detail page final String detailsPath; + /// The path to another detail page + final String? detailsPath2; + @override Widget build(BuildContext context) { return Scaffold( @@ -347,6 +226,14 @@ class RootScreen extends StatelessWidget { }, child: const Text('View details'), ), + const Padding(padding: EdgeInsets.all(4)), + if (detailsPath2 != null) + TextButton( + onPressed: () { + GoRouter.of(context).go(detailsPath2!); + }, + child: const Text('View more details'), + ), ], ), ), @@ -359,12 +246,16 @@ class DetailsScreen extends StatefulWidget { /// Constructs a [DetailsScreen]. const DetailsScreen({ required this.label, + this.param, Key? key, }) : super(key: key); /// The label to display in the center of the screen. final String label; + /// Optional param + final String? param; + @override State createState() => DetailsScreenState(); } @@ -383,6 +274,10 @@ class DetailsScreenState extends State { child: Column( mainAxisSize: MainAxisSize.min, children: [ + if (widget.param != null) + Text('Parameter: ${widget.param!}', + style: Theme.of(context).textTheme.titleLarge), + const Padding(padding: EdgeInsets.all(4)), Text('Details for ${widget.label} - Counter: $_counter', style: Theme.of(context).textTheme.titleLarge), const Padding(padding: EdgeInsets.all(4)), diff --git a/packages/go_router/lib/go_router.dart b/packages/go_router/lib/go_router.dart index 1d9f9dd8e15d..c2ba7a18e230 100644 --- a/packages/go_router/lib/go_router.dart +++ b/packages/go_router/lib/go_router.dart @@ -7,9 +7,10 @@ library go_router; export 'src/configuration.dart' - show GoRoute, GoRouterState, RouteBase, ShellRoute; + show GoRoute, GoRouterState, RouteBase, ShellRoute, PartitionedShellRoute; export 'src/misc/extensions.dart'; export 'src/misc/inherited_router.dart'; +export 'src/misc/stacked_navigation_scaffold.dart'; export 'src/pages/custom_transition_page.dart'; export 'src/platform.dart' show UrlPathStrategy; export 'src/route_data.dart' show GoRouteData, TypedGoRoute; diff --git a/packages/go_router/lib/src/builder.dart b/packages/go_router/lib/src/builder.dart index b0c8728cdf83..976dc93721ea 100644 --- a/packages/go_router/lib/src/builder.dart +++ b/packages/go_router/lib/src/builder.dart @@ -165,41 +165,48 @@ class RouteBuilder { _buildRecursive(context, matchList, startIndex + 1, pop, routerNeglect, keyToPages, newParams, navigatorKey); - } else if (route is ShellRoute) { + } else if (route is ShellRouteBase) { // The key for the Navigator that will display this ShellRoute's page. final GlobalKey parentNavigatorKey = navigatorKey; - // The key to provide to the ShellRoute's Navigator. - final GlobalKey shellNavigatorKey = route.navigatorKey; - // Add an entry for the parent navigator if none exists. keyToPages.putIfAbsent(parentNavigatorKey, () => >[]); - // Add an entry for the shell route's navigator - keyToPages.putIfAbsent(shellNavigatorKey, () => >[]); - // Calling _buildRecursive can result in adding pages to the // parentNavigatorKey entry's list. Store the current length so // that the page for this ShellRoute is placed at the right index. final int shellPageIdx = keyToPages[parentNavigatorKey]!.length; + // The key to provide to the ShellRoute's Navigator. + final GlobalKey shellNavigatorKey; + + if (route is PartitionedShellRoute) { + // Get the next route match (sub route), which for a PartitionedShellRoute + // should always be a GoRoute with a parentNavigatorKey + final RouteBase? subRoute = (matchList.matches.length > startIndex + 1) + ? matchList.matches[startIndex + 1].route + : null; + if (subRoute is! GoRoute || subRoute.parentNavigatorKey == null) { + throw _RouteBuilderError( + 'Direct descendants of PartitionedShellRoute ' + '($route) must be GoRoute with a parentNavigatorKey.'); + } + shellNavigatorKey = subRoute.parentNavigatorKey!; + } else if (route is ShellRoute) { + shellNavigatorKey = route.navigatorKey; + } else { + throw _RouteBuilderError('Unknown route type: $route'); + } + + // Add an entry for the shell route's navigator + keyToPages.putIfAbsent(shellNavigatorKey, () => >[]); + // Build the remaining pages _buildRecursive(context, matchList, startIndex + 1, pop, routerNeglect, keyToPages, newParams, shellNavigatorKey); - final Widget child; - final ShellRouteNestedNavigationBuilder? nestedNavigationBuilder = - route.nestedNavigationBuilder; - if (nestedNavigationBuilder != null) { - // Build the container for nested routes (delegate responsibility for - // building nested Navigator) - child = nestedNavigationBuilder( - context, state, keyToPages[shellNavigatorKey]!); - } else { - // Build the Navigator - child = _buildNavigator( - pop, keyToPages[shellNavigatorKey]!, shellNavigatorKey); - } + final Widget child = _buildNavigator( + pop, keyToPages[shellNavigatorKey]!, shellNavigatorKey); // Build the Page for this route final Page page = @@ -273,7 +280,7 @@ class RouteBuilder { if (pageBuilder != null) { page = pageBuilder(context, state); } - } else if (route is ShellRoute) { + } else if (route is ShellRouteBase) { final ShellRoutePageBuilder? pageBuilder = route.pageBuilder; assert(child != null, 'ShellRoute must contain a child route'); if (pageBuilder != null) { @@ -309,18 +316,12 @@ class RouteBuilder { } return builder(context, state); - } else if (route is ShellRoute) { + } else if (route is ShellRouteBase) { if (childWidget == null) { throw _RouteBuilderException( 'Attempt to build ShellRoute without a child widget'); } - // When `nestedNavigationBuilder` is set it supersedes `builder`, and at - // this point it will already have been used to create `childWidget`. - if (route.nestedNavigationBuilder != null) { - return childWidget; - } - final ShellRouteBuilder? builder = route.builder; if (builder == null) { diff --git a/packages/go_router/lib/src/configuration.dart b/packages/go_router/lib/src/configuration.dart index fa7261b470f9..06515b18c86b 100644 --- a/packages/go_router/lib/src/configuration.dart +++ b/packages/go_router/lib/src/configuration.dart @@ -30,7 +30,7 @@ class RouteConfiguration { if (route is GoRoute && !route.path.startsWith('/')) { assert(route.path.startsWith('/'), 'top-level path must start with "/": ${route.path}'); - } else if (route is ShellRoute) { + } else if (route is ShellRouteBase) { for (final RouteBase route in routes) { if (route is GoRoute) { assert(route.path.startsWith('/'), @@ -80,6 +80,14 @@ class RouteConfiguration { ...allowedKeys..add(route.navigatorKey) ], ); + } else if (route is PartitionedShellRoute) { + checkParentNavigatorKeys( + route.routes, + >[ + ...allowedKeys, + ...route.navigationKeys, + ], + ); } } } @@ -194,7 +202,7 @@ class RouteConfiguration { if (route.routes.isNotEmpty) { _cacheNameToPath(fullPath, route.routes); } - } else if (route is ShellRoute) { + } else if (route is ShellRouteBase) { if (route.routes.isNotEmpty) { _cacheNameToPath(parentFullPath, route.routes); } diff --git a/packages/go_router/lib/src/delegate.dart b/packages/go_router/lib/src/delegate.dart index f4ce2ce48fe0..73396356b8db 100644 --- a/packages/go_router/lib/src/delegate.dart +++ b/packages/go_router/lib/src/delegate.dart @@ -82,7 +82,7 @@ class GoRouterDelegate extends RouterDelegate /// Pushes the given location onto the page stack void push(RouteMatch match) { - if (match.route is ShellRoute) { + if (match.route is ShellRouteBase) { throw GoError('ShellRoutes cannot be pushed'); } @@ -122,12 +122,6 @@ class GoRouterDelegate extends RouterDelegate assert(canPop); return canPop; } else if (route is ShellRoute) { - if (route.nestedNavigationBuilder != null) { - // When nestedNavigationBuilder is set on a ShellRoute, the navigation - // handling is delegated, meaning ShellRoute doesn't have an associated - // Navigator. - continue; - } final bool canPop = route.navigatorKey.currentState!.canPop(); if (canPop) { return canPop; diff --git a/packages/go_router/lib/src/match.dart b/packages/go_router/lib/src/match.dart index 7d0849211943..622b659782ea 100644 --- a/packages/go_router/lib/src/match.dart +++ b/packages/go_router/lib/src/match.dart @@ -43,7 +43,7 @@ class RouteMatch { required Map> queryParametersAll, required Object? extra, }) { - if (route is ShellRoute) { + if (route is ShellRouteBase) { return RouteMatch( route: route, subloc: restLoc, diff --git a/packages/go_router/lib/src/matching.dart b/packages/go_router/lib/src/matching.dart index 1775c710f4b6..0de4e6ed7c5a 100644 --- a/packages/go_router/lib/src/matching.dart +++ b/packages/go_router/lib/src/matching.dart @@ -77,7 +77,7 @@ class RouteMatchList { _debugAssertNotEmpty(); // Also pop ShellRoutes when there are no subsequent route matches - while (_matches.isNotEmpty && _matches.last.route is ShellRoute) { + while (_matches.isNotEmpty && _matches.last.route is ShellRouteBase) { _matches.removeLast(); } @@ -145,7 +145,7 @@ List _getLocRouteRecursively({ late final String fullpath; if (route is GoRoute) { fullpath = concatenatePaths(parentFullpath, route.path); - } else if (route is ShellRoute) { + } else if (route is ShellRouteBase) { fullpath = parentFullpath; } @@ -176,7 +176,7 @@ List _getLocRouteRecursively({ // Otherwise, recurse final String childRestLoc; final String newParentSubLoc; - if (match.route is ShellRoute) { + if (match.route is ShellRouteBase) { childRestLoc = restLoc; newParentSubLoc = parentSubloc; } else { diff --git a/packages/go_router/lib/src/misc/stacked_navigation_scaffold.dart b/packages/go_router/lib/src/misc/stacked_navigation_scaffold.dart new file mode 100644 index 000000000000..9f570164b319 --- /dev/null +++ b/packages/go_router/lib/src/misc/stacked_navigation_scaffold.dart @@ -0,0 +1,207 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:collection/collection.dart'; +import 'package:flutter/widgets.dart'; + +import '../state.dart'; + +/// Transition builder callback used by [IndexStackShell]. +/// +/// The builder is expected to return a transition powered by the provided +/// `animation` and wrapping the provided `child`. +/// +/// The `animation` provided to the builder always runs forward from 0.0 to 1.0. +typedef IndexStackTransitionBuilder = Widget Function( + BuildContext context, + Animation animation, + Widget child, +); + +/// Builder for the scaffold of a [StackedNavigationScaffold] +typedef IndexStackShellScaffoldBuilder = Widget Function( + BuildContext context, + int currentIndex, + List itemsState, + Widget scaffoldBody); + +/// Representation of a item in the stack of a [StackedNavigationScaffold] +class StackedNavigationItem { + /// Constructs an [StackedNavigationItem]. + StackedNavigationItem( + {required this.initialLocation, required this.navigatorKey}); + + /// The initial location/path + final String initialLocation; + + /// Optional navigatorKey + final GlobalKey navigatorKey; +} + +/// Represents the current state of a [StackedNavigationItem] in a +/// [StackedNavigationScaffold] +class StackedNavigationItemState { + /// Constructs an [StackedNavigationItemState]. + StackedNavigationItemState(this.item); + + /// The [StackedNavigationItem] this state is representing. + final StackedNavigationItem item; + + /// The current [GoRouterState] for the navigator of this item. + GoRouterState? currentRouterState; + + /// The [Navigator] for this item. + Navigator? navigator; + + /// Gets the current location from the [currentRouterState] or falls back to + /// the initial location of the associated [item]. + String get currentLocation => currentRouterState != null + ? currentRouterState!.location + : item.initialLocation; +} + +/// Widget that maintains a stateful stack of [Navigator]s, using an +/// [IndexStack]. +class StackedNavigationScaffold extends StatefulWidget { + /// Constructs an [IndexStackShell]. + const StackedNavigationScaffold({ + required this.currentNavigator, + required this.currentRouterState, + required this.stackItems, + this.scaffoldBuilder, + this.transitionBuilder, + this.transitionDuration = defaultTransitionDuration, + super.key, + }); + + /// The default transition duration + static const Duration defaultTransitionDuration = Duration(milliseconds: 400); + + /// The navigator for the currently active tab + final Navigator currentNavigator; + + /// The current router state + final GoRouterState currentRouterState; + + /// The tabs + final List stackItems; + + /// The scaffold builder + final IndexStackShellScaffoldBuilder? scaffoldBuilder; + + /// An optional transition builder for stack transitions + final IndexStackTransitionBuilder? transitionBuilder; + + /// The duration for stack transitions + final Duration transitionDuration; + + @override + State createState() => _StackedNavigationScaffoldState(); +} + +class _StackedNavigationScaffoldState extends State + with SingleTickerProviderStateMixin { + int _currentIndex = 0; + late final AnimationController? _animationController; + late final List _items; + + int _findCurrentIndex() { + final int index = _items.indexWhere((StackedNavigationItemState i) => + i.item.navigatorKey == widget.currentNavigator.key); + return index < 0 ? 0 : index; + } + + @override + void initState() { + super.initState(); + _items = widget.stackItems + .map((StackedNavigationItem i) => StackedNavigationItemState(i)) + .toList(); + + if (widget.transitionBuilder != null) { + _animationController = + AnimationController(vsync: this, duration: widget.transitionDuration); + _animationController?.forward(); + } else { + _animationController = null; + } + } + + @override + void didUpdateWidget(covariant StackedNavigationScaffold oldWidget) { + super.didUpdateWidget(oldWidget); + _updateForCurrentTab(); + } + + @override + void didChangeDependencies() { + super.didChangeDependencies(); + _updateForCurrentTab(); + } + + void _updateForCurrentTab() { + final int previousIndex = _currentIndex; + _currentIndex = _findCurrentIndex(); + + final StackedNavigationItemState itemState = _items[_currentIndex]; + itemState.navigator = widget.currentNavigator; + itemState.currentRouterState = widget.currentRouterState; + + if (previousIndex != _currentIndex) { + _animationController?.forward(from: 0.0); + } + } + + @override + void dispose() { + _animationController?.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final IndexStackShellScaffoldBuilder? scaffoldBuilder = + widget.scaffoldBuilder; + if (scaffoldBuilder != null) { + return scaffoldBuilder( + context, _currentIndex, _items, _buildIndexStack(context)); + } else { + return _buildIndexStack(context); + } + } + + Widget _buildIndexStack(BuildContext context) { + final List children = _items + .mapIndexed((int index, StackedNavigationItemState item) => + _buildNavigator(context, index, item)) + .toList(); + + final Widget indexedStack = + IndexedStack(index: _currentIndex, children: children); + + final IndexStackTransitionBuilder? transitionBuilder = + widget.transitionBuilder; + if (transitionBuilder != null) { + return transitionBuilder(context, _animationController!, indexedStack); + } else { + return indexedStack; + } + } + + Widget _buildNavigator(BuildContext context, int index, + StackedNavigationItemState navigationItem) { + final Navigator? navigator = navigationItem.navigator; + if (navigator == null) { + return const SizedBox.shrink(); + } + final bool isActive = index == _currentIndex; + return Offstage( + offstage: !isActive, + child: TickerMode( + enabled: isActive, + child: navigator, + ), + ); + } +} diff --git a/packages/go_router/lib/src/route.dart b/packages/go_router/lib/src/route.dart index 999b1010b8f9..812ef12dd069 100644 --- a/packages/go_router/lib/src/route.dart +++ b/packages/go_router/lib/src/route.dart @@ -6,6 +6,7 @@ import 'package:collection/collection.dart'; import 'package:flutter/widgets.dart'; import 'configuration.dart'; +import 'misc/stacked_navigation_scaffold.dart'; import 'pages/custom_transition_page.dart'; import 'path_utils.dart'; import 'typedefs.dart'; @@ -333,6 +334,27 @@ class GoRoute extends RouteBase { late final RegExp _pathRE; } +/// Base class for classes that acts as a shell for child routes, such +/// as [ShellRoute] and [PartitionedShellRoute]. +abstract class ShellRouteBase extends RouteBase { + const ShellRouteBase._({this.builder, this.pageBuilder, super.routes}) + : super._(); + + /// The widget builder for a shell route. + /// + /// Similar to GoRoute builder, but with an additional child parameter. This + /// child parameter is the Widget built by calling the matching sub-route's + /// builder. + final ShellRouteBuilder? builder; + + /// The page builder for a shell route. + /// + /// Similar to GoRoute pageBuilder, but with an additional child parameter. + /// This child parameter is the Widget built by calling the matching + /// sub-route's builder. + final ShellRoutePageBuilder? pageBuilder; +} + /// A route that displays a UI shell around the matching child route. /// /// When a ShellRoute is added to the list of routes on GoRouter or GoRoute, a @@ -345,15 +367,6 @@ class GoRoute extends RouteBase { /// passed to the /b/details route so that it displays on the root Navigator /// instead of the ShellRoute's Navigator: /// -/// To delegate nested navigation handling entirely to a child of this route, -/// specify [nestedNavigationBuilder] instead of [builder] or [pageBuilder]. -/// Doing so means no [Navigator] will be built by this route. This is -/// convenient when for instance implementing a UI with a [BottomNavigationBar], -/// with a persistent navigation state for each tab (i.e. building a [Navigator] -/// for each tab). -/// See [Stateful Nested Navigation](https://github.com/flutter/packages/blob/main/packages/go_router/example/lib/stateful_nested_navigation.dart) -/// for a complete runnable example. -/// /// ``` /// final GlobalKey _rootNavigatorKey = /// GlobalKey(); @@ -437,57 +450,87 @@ class GoRoute extends RouteBase { /// ), /// ``` /// -class ShellRoute extends RouteBase { +class ShellRoute extends ShellRouteBase { /// Constructs a [ShellRoute]. ShellRoute({ - this.builder, - this.pageBuilder, - this.nestedNavigationBuilder, + super.builder, + super.pageBuilder, super.routes, GlobalKey? navigatorKey, }) : assert(routes.isNotEmpty), navigatorKey = navigatorKey ?? GlobalKey(), + //nestedNavigationBuilder = null, super._() { for (final RouteBase route in routes) { if (route is GoRoute) { assert(route.parentNavigatorKey == null); } } - if (nestedNavigationBuilder != null) { - assert(builder == null, - 'Cannot use both builder and nestedNavigationBuilder'); - } } - /// The widget builder for a shell route. - /// - /// Similar to GoRoute builder, but with an additional child parameter. This - /// child parameter is the Widget built by calling the matching sub-route's - /// builder. - final ShellRouteBuilder? builder; + /// The [GlobalKey] to be used by the [Navigator] built for this route. + /// All ShellRoutes build a Navigator by default. Child GoRoutes + /// are placed onto this Navigator instead of the root Navigator. + final GlobalKey navigatorKey; +} - /// The page builder for a shell route. - /// - /// Similar to GoRoute pageBuilder, but with an additional child parameter. - /// This child parameter is the Widget built by calling the matching - /// sub-route's builder. - final ShellRoutePageBuilder? pageBuilder; +/// A route that displays a UI shell with separate [Navigator]s for its child +/// routes. +/// +/// When using this route class as a parent shell route, it possible to build +/// a stateful nested navigation. This is convenient when for instance +/// implementing a UI with a [BottomNavigationBar], with a persistent navigation +/// state for each tab +/// +/// See [Stateful Nested Navigation](https://github.com/flutter/packages/blob/main/packages/go_router/example/lib/stateful_nested_navigation.dart) +/// for a complete runnable example. +class PartitionedShellRoute extends ShellRouteBase { + /// Constructs a [PartitionedShellRoute] from a list of [GoRoute], that will + /// represent the roots in a stacked navigation hierarchy. For each root route, + /// a separate [Navigator] will be created, using the navigation key specified + /// by the `parentNavigatorKey` property of [GoRoute]. This navigation key must + /// be one of the keys specified in [navigationKeys]. + PartitionedShellRoute({ + required List routes, + required this.navigationKeys, + super.builder, + super.pageBuilder, + }) : assert(routes.isNotEmpty), + assert(navigationKeys.length == routes.length), + super._(routes: routes) { + for (final GoRoute route in routes) { + assert(navigationKeys.contains(route.parentNavigatorKey)); + } + } - /// The widget builder for a nested navigation shell route. - /// - /// Similar to GoRoute builder, but with an additional pages parameter. This - /// pages parameter contains the pages that should currently be displayed in - /// a nested navigation. This nested navigation builder is a replacement for - /// [builder] (i.e. both cannot be used as the same time). - final ShellRouteNestedNavigationBuilder? nestedNavigationBuilder; + /// Constructs a [PartitionedShellRoute] that manages its navigators in form of + /// a stack, using [StackedNavigationScaffold]. + factory PartitionedShellRoute.stackedNavigation({ + required List routes, + required List stackItems, + IndexStackShellScaffoldBuilder? scaffoldBuilder, + IndexStackTransitionBuilder? transitionBuilder, + Duration? transitionDuration, + }) { + return PartitionedShellRoute( + routes: routes, + navigationKeys: stackItems + .map((StackedNavigationItem e) => e.navigatorKey) + .toList(), + builder: (BuildContext context, GoRouterState state, + Widget currentTabNavigator) { + return StackedNavigationScaffold( + currentNavigator: currentTabNavigator as Navigator, + currentRouterState: state, + stackItems: stackItems, + scaffoldBuilder: scaffoldBuilder, + transitionBuilder: transitionBuilder, + transitionDuration: transitionDuration ?? + StackedNavigationScaffold.defaultTransitionDuration, + ); + }); + } - /// The [GlobalKey] to be used by the [Navigator] built for this route. - /// - /// Unless [nestedNavigationBuilder] is set, all ShellRoutes build a Navigator - /// by default. Child GoRoutes are placed onto this Navigator instead of the - /// root Navigator. However, if [nestedNavigationBuilder] is set, construction - /// of a nested Navigator is instead delegated to the widget created by that - /// builder. In that scenario, this property will not be associated with a - /// Navigator, and should not be used to access NavigatorState. - final GlobalKey navigatorKey; + /// The navigator keys of the navigators created by this route. + final List> navigationKeys; } diff --git a/packages/go_router/lib/src/typedefs.dart b/packages/go_router/lib/src/typedefs.dart index 85c6db3a77e4..f89448876b36 100644 --- a/packages/go_router/lib/src/typedefs.dart +++ b/packages/go_router/lib/src/typedefs.dart @@ -34,14 +34,6 @@ typedef ShellRoutePageBuilder = Page Function( Widget child, ); -/// A builder used for customizing the creation of nested navigation in a -/// [ShellRoute]. -typedef ShellRouteNestedNavigationBuilder = Widget Function( - BuildContext context, - GoRouterState state, - List> pages, -); - /// The signature of the navigatorBuilder callback. typedef GoRouterNavigatorBuilder = Widget Function( BuildContext context, diff --git a/packages/go_router/test/builder_test.dart b/packages/go_router/test/builder_test.dart index 0c0fbc5ccbbb..3032d7a002a6 100644 --- a/packages/go_router/test/builder_test.dart +++ b/packages/go_router/test/builder_test.dart @@ -100,19 +100,19 @@ void main() { testWidgets('Builds ShellRoute with nestedNavigationBuilder', (WidgetTester tester) async { + final GlobalKey key = GlobalKey(); final RouteConfiguration config = RouteConfiguration( routes: [ - ShellRoute( - nestedNavigationBuilder: (BuildContext context, - GoRouterState state, List> pages) { - return Navigator( - pages: pages, - onPopPage: (Route route, dynamic result) { - return false; - }); - }, + PartitionedShellRoute( + builder: + (BuildContext context, GoRouterState state, Widget child) => + child, + navigationKeys: [ + key + ], routes: [ GoRoute( + parentNavigatorKey: key, path: '/nested', builder: (BuildContext context, GoRouterState state) { return _DetailsScreen(); diff --git a/packages/go_router/test/configuration_test.dart b/packages/go_router/test/configuration_test.dart index f306029a530a..61f769f4c539 100644 --- a/packages/go_router/test/configuration_test.dart +++ b/packages/go_router/test/configuration_test.dart @@ -80,37 +80,6 @@ void main() { ); }); - test( - 'throws when ShellRoute has both a builder and a nestedNavigationBuilder', - () { - final GlobalKey root = - GlobalKey(debugLabel: 'root'); - final List shellRouteChildren = [ - GoRoute( - path: '/a', - builder: _mockScreenBuilder, - ) - ]; - expect( - () { - RouteConfiguration( - navigatorKey: root, - routes: [ - ShellRoute( - routes: shellRouteChildren, - builder: _mockShellBuilder, - nestedNavigationBuilder: _mockShellNavigationBuilder), - ], - redirectLimit: 10, - topRedirect: (BuildContext context, GoRouterState state) { - return null; - }, - ); - }, - throwsAssertionError, - ); - }); - test( 'throws when there is a GoRoute ancestor with a different parentNavigatorKey', () { @@ -500,7 +469,3 @@ Widget _mockScreenBuilder(BuildContext context, GoRouterState state) => Widget _mockShellBuilder( BuildContext context, GoRouterState state, Widget child) => child; - -Widget _mockShellNavigationBuilder( - BuildContext context, GoRouterState state, List> pages) => - _MockScreen(key: state.pageKey); From 510ec34d5e4108a68c10dd88b71064a8d018e0aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20Lo=CC=88fstrand?= Date: Fri, 30 Sep 2022 05:25:29 +0200 Subject: [PATCH 012/112] Some renaming. --- .../lib/src/misc/stacked_navigation_scaffold.dart | 12 ++++++------ packages/go_router/lib/src/route.dart | 4 ++-- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/go_router/lib/src/misc/stacked_navigation_scaffold.dart b/packages/go_router/lib/src/misc/stacked_navigation_scaffold.dart index 9f570164b319..e01047ec13ae 100644 --- a/packages/go_router/lib/src/misc/stacked_navigation_scaffold.dart +++ b/packages/go_router/lib/src/misc/stacked_navigation_scaffold.dart @@ -13,14 +13,14 @@ import '../state.dart'; /// `animation` and wrapping the provided `child`. /// /// The `animation` provided to the builder always runs forward from 0.0 to 1.0. -typedef IndexStackTransitionBuilder = Widget Function( +typedef StackedNavigationTransitionBuilder = Widget Function( BuildContext context, Animation animation, Widget child, ); /// Builder for the scaffold of a [StackedNavigationScaffold] -typedef IndexStackShellScaffoldBuilder = Widget Function( +typedef StackedNavigationScaffoldBuilder = Widget Function( BuildContext context, int currentIndex, List itemsState, @@ -88,10 +88,10 @@ class StackedNavigationScaffold extends StatefulWidget { final List stackItems; /// The scaffold builder - final IndexStackShellScaffoldBuilder? scaffoldBuilder; + final StackedNavigationScaffoldBuilder? scaffoldBuilder; /// An optional transition builder for stack transitions - final IndexStackTransitionBuilder? transitionBuilder; + final StackedNavigationTransitionBuilder? transitionBuilder; /// The duration for stack transitions final Duration transitionDuration; @@ -161,7 +161,7 @@ class _StackedNavigationScaffoldState extends State @override Widget build(BuildContext context) { - final IndexStackShellScaffoldBuilder? scaffoldBuilder = + final StackedNavigationScaffoldBuilder? scaffoldBuilder = widget.scaffoldBuilder; if (scaffoldBuilder != null) { return scaffoldBuilder( @@ -180,7 +180,7 @@ class _StackedNavigationScaffoldState extends State final Widget indexedStack = IndexedStack(index: _currentIndex, children: children); - final IndexStackTransitionBuilder? transitionBuilder = + final StackedNavigationTransitionBuilder? transitionBuilder = widget.transitionBuilder; if (transitionBuilder != null) { return transitionBuilder(context, _animationController!, indexedStack); diff --git a/packages/go_router/lib/src/route.dart b/packages/go_router/lib/src/route.dart index 812ef12dd069..521ef441ddbb 100644 --- a/packages/go_router/lib/src/route.dart +++ b/packages/go_router/lib/src/route.dart @@ -508,8 +508,8 @@ class PartitionedShellRoute extends ShellRouteBase { factory PartitionedShellRoute.stackedNavigation({ required List routes, required List stackItems, - IndexStackShellScaffoldBuilder? scaffoldBuilder, - IndexStackTransitionBuilder? transitionBuilder, + StackedNavigationScaffoldBuilder? scaffoldBuilder, + StackedNavigationTransitionBuilder? transitionBuilder, Duration? transitionDuration, }) { return PartitionedShellRoute( From e82647a0f44d2f9b670698237dd2290b35aedd8e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20Lo=CC=88fstrand?= Date: Fri, 30 Sep 2022 14:07:40 +0200 Subject: [PATCH 013/112] Changed the way currentLocation is calculated since it currently doesn't seem possible to use GoRouterState for this (after performing pop at least). Made transitionDuration optional instead of using default value. --- .../src/misc/stacked_navigation_scaffold.dart | 37 +++++++++++-------- 1 file changed, 21 insertions(+), 16 deletions(-) diff --git a/packages/go_router/lib/src/misc/stacked_navigation_scaffold.dart b/packages/go_router/lib/src/misc/stacked_navigation_scaffold.dart index e01047ec13ae..48374fe52a6d 100644 --- a/packages/go_router/lib/src/misc/stacked_navigation_scaffold.dart +++ b/packages/go_router/lib/src/misc/stacked_navigation_scaffold.dart @@ -5,9 +5,10 @@ import 'package:collection/collection.dart'; import 'package:flutter/widgets.dart'; +import '../router.dart'; import '../state.dart'; -/// Transition builder callback used by [IndexStackShell]. +/// Transition builder callback used by [StackedNavigationScaffold]. /// /// The builder is expected to return a transition powered by the provided /// `animation` and wrapping the provided `child`. @@ -30,10 +31,10 @@ typedef StackedNavigationScaffoldBuilder = Widget Function( class StackedNavigationItem { /// Constructs an [StackedNavigationItem]. StackedNavigationItem( - {required this.initialLocation, required this.navigatorKey}); + {required this.rootRoutePath, required this.navigatorKey}); - /// The initial location/path - final String initialLocation; + /// The location/path of the root route of this navigation tree + final String rootRoutePath; /// Optional navigatorKey final GlobalKey navigatorKey; @@ -48,30 +49,29 @@ class StackedNavigationItemState { /// The [StackedNavigationItem] this state is representing. final StackedNavigationItem item; - /// The current [GoRouterState] for the navigator of this item. - GoRouterState? currentRouterState; + /// The last location of this item. + String? lastLocation; /// The [Navigator] for this item. Navigator? navigator; /// Gets the current location from the [currentRouterState] or falls back to - /// the initial location of the associated [item]. - String get currentLocation => currentRouterState != null - ? currentRouterState!.location - : item.initialLocation; + /// the root route location of the associated [item]. + String get currentLocation => + lastLocation != null ? lastLocation! : item.rootRoutePath; } /// Widget that maintains a stateful stack of [Navigator]s, using an /// [IndexStack]. class StackedNavigationScaffold extends StatefulWidget { - /// Constructs an [IndexStackShell]. + /// Constructs an [StackedNavigationScaffold]. const StackedNavigationScaffold({ required this.currentNavigator, required this.currentRouterState, required this.stackItems, this.scaffoldBuilder, this.transitionBuilder, - this.transitionDuration = defaultTransitionDuration, + this.transitionDuration, super.key, }); @@ -94,7 +94,7 @@ class StackedNavigationScaffold extends StatefulWidget { final StackedNavigationTransitionBuilder? transitionBuilder; /// The duration for stack transitions - final Duration transitionDuration; + final Duration? transitionDuration; @override State createState() => _StackedNavigationScaffoldState(); @@ -120,8 +120,10 @@ class _StackedNavigationScaffoldState extends State .toList(); if (widget.transitionBuilder != null) { - _animationController = - AnimationController(vsync: this, duration: widget.transitionDuration); + _animationController = AnimationController( + vsync: this, + duration: widget.transitionDuration ?? + StackedNavigationScaffold.defaultTransitionDuration); _animationController?.forward(); } else { _animationController = null; @@ -146,7 +148,10 @@ class _StackedNavigationScaffoldState extends State final StackedNavigationItemState itemState = _items[_currentIndex]; itemState.navigator = widget.currentNavigator; - itemState.currentRouterState = widget.currentRouterState; + // Note: Would have been cleaner to be able to get the current location + // (full path) from GoRouterState, but currently that isn't possible, since + // the RouteMatchList doesn't seem to be updated properly on pop. + itemState.lastLocation = GoRouter.of(context).location; if (previousIndex != _currentIndex) { _animationController?.forward(from: 0.0); From e38fa3203b5354b7d61eb0bf9249f3e8bc1ca82d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20Lo=CC=88fstrand?= Date: Fri, 30 Sep 2022 14:07:59 +0200 Subject: [PATCH 014/112] Minor cleanup and refactoring. --- .../example/lib/stateful_nested_navigation.dart | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/packages/go_router/example/lib/stateful_nested_navigation.dart b/packages/go_router/example/lib/stateful_nested_navigation.dart index a45408188454..a59fcda7ca75 100644 --- a/packages/go_router/example/lib/stateful_nested_navigation.dart +++ b/packages/go_router/example/lib/stateful_nested_navigation.dart @@ -35,12 +35,12 @@ class NestedTabNavigationExampleApp extends StatelessWidget { [ ScaffoldWithNavBarTabItem( navigationItem: StackedNavigationItem( - initialLocation: '/a', navigatorKey: _sectionANavigatorKey), + rootRoutePath: '/a', navigatorKey: _sectionANavigatorKey), icon: const Icon(Icons.home), label: 'Section A'), ScaffoldWithNavBarTabItem( navigationItem: StackedNavigationItem( - initialLocation: '/b', navigatorKey: _sectionBNavigatorKey), + rootRoutePath: '/b', navigatorKey: _sectionBNavigatorKey), icon: const Icon(Icons.settings), label: 'Section B', ), @@ -64,7 +64,6 @@ class NestedTabNavigationExampleApp extends StatelessWidget { itemsState: itemsState, body: scaffoldBody); }, - /// A transition builder is optional, only included here for /// demonstration purposes. transitionBuilder: @@ -124,9 +123,7 @@ class NestedTabNavigationExampleApp extends StatelessWidget { theme: ThemeData( primarySwatch: Colors.blue, ), - routeInformationParser: _router.routeInformationParser, - routerDelegate: _router.routerDelegate, - routeInformationProvider: _router.routeInformationProvider, + routerConfig: _router, ); } } From f38b9bf4e0cd5a2debdf1163f1dac713d3cf834a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20Lo=CC=88fstrand?= Date: Fri, 30 Sep 2022 15:30:38 +0200 Subject: [PATCH 015/112] Renamed StackedNavigationScaffold to StackedNavigationShell. Minor clean up, assertion and doc updates. --- packages/go_router/lib/go_router.dart | 2 +- ...old.dart => stacked_navigation_shell.dart} | 40 ++++++++++++------- packages/go_router/lib/src/route.dart | 22 ++++++---- 3 files changed, 40 insertions(+), 24 deletions(-) rename packages/go_router/lib/src/misc/{stacked_navigation_scaffold.dart => stacked_navigation_shell.dart} (82%) diff --git a/packages/go_router/lib/go_router.dart b/packages/go_router/lib/go_router.dart index c2ba7a18e230..b2fc804c6f08 100644 --- a/packages/go_router/lib/go_router.dart +++ b/packages/go_router/lib/go_router.dart @@ -10,7 +10,7 @@ export 'src/configuration.dart' show GoRoute, GoRouterState, RouteBase, ShellRoute, PartitionedShellRoute; export 'src/misc/extensions.dart'; export 'src/misc/inherited_router.dart'; -export 'src/misc/stacked_navigation_scaffold.dart'; +export 'src/misc/stacked_navigation_shell.dart'; export 'src/pages/custom_transition_page.dart'; export 'src/platform.dart' show UrlPathStrategy; export 'src/route_data.dart' show GoRouteData, TypedGoRoute; diff --git a/packages/go_router/lib/src/misc/stacked_navigation_scaffold.dart b/packages/go_router/lib/src/misc/stacked_navigation_shell.dart similarity index 82% rename from packages/go_router/lib/src/misc/stacked_navigation_scaffold.dart rename to packages/go_router/lib/src/misc/stacked_navigation_shell.dart index 48374fe52a6d..0685019a009b 100644 --- a/packages/go_router/lib/src/misc/stacked_navigation_scaffold.dart +++ b/packages/go_router/lib/src/misc/stacked_navigation_shell.dart @@ -8,7 +8,7 @@ import 'package:flutter/widgets.dart'; import '../router.dart'; import '../state.dart'; -/// Transition builder callback used by [StackedNavigationScaffold]. +/// Transition builder callback used by [StackedNavigationShell]. /// /// The builder is expected to return a transition powered by the provided /// `animation` and wrapping the provided `child`. @@ -20,14 +20,15 @@ typedef StackedNavigationTransitionBuilder = Widget Function( Widget child, ); -/// Builder for the scaffold of a [StackedNavigationScaffold] +/// Builder for the scaffold of a [StackedNavigationShell] typedef StackedNavigationScaffoldBuilder = Widget Function( - BuildContext context, - int currentIndex, - List itemsState, - Widget scaffoldBody); + BuildContext context, + int currentIndex, + List itemsState, + Widget scaffoldBody, +); -/// Representation of a item in the stack of a [StackedNavigationScaffold] +/// Representation of a item in the stack of a [StackedNavigationShell] class StackedNavigationItem { /// Constructs an [StackedNavigationItem]. StackedNavigationItem( @@ -41,7 +42,7 @@ class StackedNavigationItem { } /// Represents the current state of a [StackedNavigationItem] in a -/// [StackedNavigationScaffold] +/// [StackedNavigationShell] class StackedNavigationItemState { /// Constructs an [StackedNavigationItemState]. StackedNavigationItemState(this.item); @@ -63,9 +64,18 @@ class StackedNavigationItemState { /// Widget that maintains a stateful stack of [Navigator]s, using an /// [IndexStack]. -class StackedNavigationScaffold extends StatefulWidget { - /// Constructs an [StackedNavigationScaffold]. - const StackedNavigationScaffold({ +/// +/// Each item in the stack is represented by a [StackedNavigationItem], +/// specified in the `stackItems` parameter. The stack items will be used to +/// build the widgets containing the [Navigator] for each index in the stack. +/// Once a stack item (along with its Navigator) has been initialized, it will +/// remain in a widget tree, wrapped in an [Offstage] widget. +/// +/// The stacked navigation shell can be customized by specifying a +/// `scaffoldBuilder`, to build a widget that wraps the index stack. +class StackedNavigationShell extends StatefulWidget { + /// Constructs an [StackedNavigationShell]. + const StackedNavigationShell({ required this.currentNavigator, required this.currentRouterState, required this.stackItems, @@ -97,10 +107,10 @@ class StackedNavigationScaffold extends StatefulWidget { final Duration? transitionDuration; @override - State createState() => _StackedNavigationScaffoldState(); + State createState() => _StackedNavigationShellState(); } -class _StackedNavigationScaffoldState extends State +class _StackedNavigationShellState extends State with SingleTickerProviderStateMixin { int _currentIndex = 0; late final AnimationController? _animationController; @@ -123,7 +133,7 @@ class _StackedNavigationScaffoldState extends State _animationController = AnimationController( vsync: this, duration: widget.transitionDuration ?? - StackedNavigationScaffold.defaultTransitionDuration); + StackedNavigationShell.defaultTransitionDuration); _animationController?.forward(); } else { _animationController = null; @@ -131,7 +141,7 @@ class _StackedNavigationScaffoldState extends State } @override - void didUpdateWidget(covariant StackedNavigationScaffold oldWidget) { + void didUpdateWidget(covariant StackedNavigationShell oldWidget) { super.didUpdateWidget(oldWidget); _updateForCurrentTab(); } diff --git a/packages/go_router/lib/src/route.dart b/packages/go_router/lib/src/route.dart index 521ef441ddbb..7258805cac84 100644 --- a/packages/go_router/lib/src/route.dart +++ b/packages/go_router/lib/src/route.dart @@ -6,7 +6,7 @@ import 'package:collection/collection.dart'; import 'package:flutter/widgets.dart'; import 'configuration.dart'; -import 'misc/stacked_navigation_scaffold.dart'; +import 'misc/stacked_navigation_shell.dart'; import 'pages/custom_transition_page.dart'; import 'path_utils.dart'; import 'typedefs.dart'; @@ -497,14 +497,21 @@ class PartitionedShellRoute extends ShellRouteBase { super.pageBuilder, }) : assert(routes.isNotEmpty), assert(navigationKeys.length == routes.length), + assert(pageBuilder != null || builder != null, + 'builder or pageBuilder must be provided'), super._(routes: routes) { - for (final GoRoute route in routes) { - assert(navigationKeys.contains(route.parentNavigatorKey)); + for (int i = 0; i < routes.length; ++i) { + assert(routes[i].parentNavigatorKey == navigationKeys[i]); } } - /// Constructs a [PartitionedShellRoute] that manages its navigators in form of - /// a stack, using [StackedNavigationScaffold]. + /// Constructs a [PartitionedShellRoute] that places its navigators in an + /// [IndexStack], managed by a [StackedNavigationShell]. + /// + /// Each route in the `routes` parameter must correspond to a + /// [StackedNavigationItem], specified in the `stackItems` parameter. + /// The stacked navigation shell can be customized by specifying a + /// `scaffoldBuilder`, to build a widget that wraps the index stack. factory PartitionedShellRoute.stackedNavigation({ required List routes, required List stackItems, @@ -519,14 +526,13 @@ class PartitionedShellRoute extends ShellRouteBase { .toList(), builder: (BuildContext context, GoRouterState state, Widget currentTabNavigator) { - return StackedNavigationScaffold( + return StackedNavigationShell( currentNavigator: currentTabNavigator as Navigator, currentRouterState: state, stackItems: stackItems, scaffoldBuilder: scaffoldBuilder, transitionBuilder: transitionBuilder, - transitionDuration: transitionDuration ?? - StackedNavigationScaffold.defaultTransitionDuration, + transitionDuration: transitionDuration, ); }); } From 89f1dd7cb93d0f075940fdcd6b49cbb6b736b864 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20Lo=CC=88fstrand?= Date: Fri, 30 Sep 2022 15:32:28 +0200 Subject: [PATCH 016/112] Added more tests for PartitionedShellRoute. --- .../lib/stateful_nested_navigation.dart | 1 + packages/go_router/test/builder_test.dart | 11 +- .../go_router/test/configuration_test.dart | 72 +++++++++ packages/go_router/test/go_router_test.dart | 150 ++++++++++++++++++ packages/go_router/test/test_helpers.dart | 6 + 5 files changed, 235 insertions(+), 5 deletions(-) diff --git a/packages/go_router/example/lib/stateful_nested_navigation.dart b/packages/go_router/example/lib/stateful_nested_navigation.dart index a59fcda7ca75..a4324a3762e5 100644 --- a/packages/go_router/example/lib/stateful_nested_navigation.dart +++ b/packages/go_router/example/lib/stateful_nested_navigation.dart @@ -64,6 +64,7 @@ class NestedTabNavigationExampleApp extends StatelessWidget { itemsState: itemsState, body: scaffoldBody); }, + /// A transition builder is optional, only included here for /// demonstration purposes. transitionBuilder: diff --git a/packages/go_router/test/builder_test.dart b/packages/go_router/test/builder_test.dart index 3032d7a002a6..8d6fb75a3a2e 100644 --- a/packages/go_router/test/builder_test.dart +++ b/packages/go_router/test/builder_test.dart @@ -98,17 +98,17 @@ void main() { expect(find.byType(_DetailsScreen), findsOneWidget); }); - testWidgets('Builds ShellRoute with nestedNavigationBuilder', - (WidgetTester tester) async { - final GlobalKey key = GlobalKey(); + testWidgets('Builds PartitionedShellRoute', (WidgetTester tester) async { + final GlobalKey key = + GlobalKey(debugLabel: 'key'); final RouteConfiguration config = RouteConfiguration( routes: [ PartitionedShellRoute( builder: (BuildContext context, GoRouterState state, Widget child) => child, - navigationKeys: [ - key + navigationKeys: >[ + key, ], routes: [ GoRoute( @@ -158,6 +158,7 @@ void main() { ); expect(find.byType(_DetailsScreen), findsOneWidget); + expect(find.byKey(key), findsOneWidget); }); testWidgets('Uses the correct navigatorKey', (WidgetTester tester) async { diff --git a/packages/go_router/test/configuration_test.dart b/packages/go_router/test/configuration_test.dart index 61f769f4c539..0a7f2357bb89 100644 --- a/packages/go_router/test/configuration_test.dart +++ b/packages/go_router/test/configuration_test.dart @@ -80,6 +80,78 @@ void main() { ); }); + test( + 'throws when a child of PartitionedShellRoute is missing a ' + 'parentNavigatorKey', () { + final GlobalKey root = + GlobalKey(debugLabel: 'root'); + final GlobalKey nested = + GlobalKey(debugLabel: 'nested'); + final List shellRouteChildren = [ + GoRoute(path: '/', builder: _mockScreenBuilder), + ]; + expect( + () { + RouteConfiguration( + navigatorKey: root, + routes: [ + PartitionedShellRoute( + routes: shellRouteChildren, + navigationKeys: >[nested], + builder: _mockShellBuilder), + ], + redirectLimit: 10, + topRedirect: (BuildContext context, GoRouterState state) { + return null; + }, + ); + }, + throwsAssertionError, + ); + }); + + test( + 'throws when a child of PartitionedShellRoute has an incorrect ' + 'parentNavigatorKey', () { + final GlobalKey root = + GlobalKey(debugLabel: 'root'); + final GlobalKey sectionANavigatorKey = + GlobalKey(); + final GlobalKey sectionBNavigatorKey = + GlobalKey(); + final List shellRouteChildren = [ + GoRoute( + path: '/a', + builder: _mockScreenBuilder, + parentNavigatorKey: sectionBNavigatorKey), + GoRoute( + path: '/b', + builder: _mockScreenBuilder, + parentNavigatorKey: sectionANavigatorKey), + ]; + expect( + () { + RouteConfiguration( + navigatorKey: root, + routes: [ + PartitionedShellRoute( + routes: shellRouteChildren, + navigationKeys: >[ + sectionANavigatorKey, + sectionBNavigatorKey + ], + builder: _mockShellBuilder), + ], + redirectLimit: 10, + topRedirect: (BuildContext context, GoRouterState state) { + return null; + }, + ); + }, + throwsAssertionError, + ); + }); + test( 'throws when there is a GoRoute ancestor with a different parentNavigatorKey', () { diff --git a/packages/go_router/test/go_router_test.dart b/packages/go_router/test/go_router_test.dart index 67d3288affdb..bad79d5ca11b 100644 --- a/packages/go_router/test/go_router_test.dart +++ b/packages/go_router/test/go_router_test.dart @@ -2207,6 +2207,156 @@ void main() { expect(find.text('Screen B'), findsOneWidget); expect(find.text('Screen C'), findsNothing); }); + + testWidgets( + 'Navigates to correct nested navigation tree in PartitionedShellRoute ' + 'and maintains state', (WidgetTester tester) async { + final GlobalKey rootNavigatorKey = + GlobalKey(); + final GlobalKey sectionANavigatorKey = + GlobalKey(); + final GlobalKey sectionBNavigatorKey = + GlobalKey(); + final GlobalKey statefulWidgetKey = + GlobalKey(); + + final List stackItems = [ + StackedNavigationItem( + rootRoutePath: '/a', navigatorKey: sectionANavigatorKey), + StackedNavigationItem( + rootRoutePath: '/b', navigatorKey: sectionBNavigatorKey), + ]; + + final List routes = [ + PartitionedShellRoute.stackedNavigation( + stackItems: stackItems, + routes: [ + GoRoute( + parentNavigatorKey: sectionANavigatorKey, + path: '/a', + builder: (BuildContext context, GoRouterState state) => + const Text('Screen A'), + routes: [ + GoRoute( + path: 'detailA', + builder: (BuildContext context, GoRouterState state) => + Column(children: [ + const Text('Screen A Detail'), + DummyStatefulWidget(key: statefulWidgetKey), + ]), + ), + ], + ), + GoRoute( + parentNavigatorKey: sectionBNavigatorKey, + path: '/b', + builder: (BuildContext context, GoRouterState state) => + const Text('Screen B'), + ), + ], + ), + ]; + + final GoRouter router = await createRouter(routes, tester, + initialLocation: '/a/detailA', navigatorKey: rootNavigatorKey); + statefulWidgetKey.currentState?.increment(); + expect(find.text('Screen A'), findsNothing); + expect(find.text('Screen A Detail'), findsOneWidget); + expect(find.text('Screen B'), findsNothing); + + router.go('/b'); + await tester.pumpAndSettle(); + expect(find.text('Screen A'), findsNothing); + expect(find.text('Screen A Detail'), findsNothing); + expect(find.text('Screen B'), findsOneWidget); + + router.go('/a/detailA'); + await tester.pumpAndSettle(); + expect(statefulWidgetKey.currentState?.counter, equals(1)); + + router.pop(); + await tester.pumpAndSettle(); + expect(find.text('Screen A'), findsOneWidget); + expect(find.text('Screen A Detail'), findsNothing); + router.go('/a/detailA'); + await tester.pumpAndSettle(); + expect(statefulWidgetKey.currentState?.counter, equals(0)); + }); + + testWidgets( + 'Pops from the correct Navigator in a PartitionedShellRoute when the ' + 'Android back button is pressed', (WidgetTester tester) async { + final GlobalKey rootNavigatorKey = + GlobalKey(); + final GlobalKey sectionANavigatorKey = + GlobalKey(); + final GlobalKey sectionBNavigatorKey = + GlobalKey(); + + final List stackItems = [ + StackedNavigationItem( + rootRoutePath: '/a', navigatorKey: sectionANavigatorKey), + StackedNavigationItem( + rootRoutePath: '/b', navigatorKey: sectionBNavigatorKey), + ]; + + final List routes = [ + PartitionedShellRoute.stackedNavigation( + stackItems: stackItems, + routes: [ + GoRoute( + parentNavigatorKey: sectionANavigatorKey, + path: '/a', + builder: (BuildContext context, GoRouterState state) => + const Text('Screen A'), + routes: [ + GoRoute( + path: 'detailA', + builder: (BuildContext context, GoRouterState state) => + const Text('Screen A Detail'), + ), + ], + ), + GoRoute( + parentNavigatorKey: sectionBNavigatorKey, + path: '/b', + builder: (BuildContext context, GoRouterState state) => + const Text('Screen B'), + routes: [ + GoRoute( + path: 'detailB', + builder: (BuildContext context, GoRouterState state) => + const Text('Screen B Detail'), + ), + ], + ), + ], + ), + ]; + + final GoRouter router = await createRouter(routes, tester, + initialLocation: '/a/detailA', navigatorKey: rootNavigatorKey); + expect(find.text('Screen A'), findsNothing); + expect(find.text('Screen A Detail'), findsOneWidget); + expect(find.text('Screen B'), findsNothing); + expect(find.text('Screen B Detail'), findsNothing); + + router.go('/b/detailB'); + await tester.pumpAndSettle(); + + expect(find.text('Screen A'), findsNothing); + expect(find.text('Screen A Detail'), findsNothing); + expect(find.text('Screen B'), findsNothing); + expect(find.text('Screen B Detail'), findsOneWidget); + + await simulateAndroidBackButton(); + await tester.pumpAndSettle(); + + expect(find.text('Screen A'), findsNothing); + expect(find.text('Screen A Detail'), findsNothing); + expect(find.text('Screen B'), findsOneWidget); + expect(find.text('Screen B Detail'), findsNothing); + }); }); group('Imperative navigation', () { diff --git a/packages/go_router/test/test_helpers.dart b/packages/go_router/test/test_helpers.dart index af9bf50db627..b807997a565e 100644 --- a/packages/go_router/test/test_helpers.dart +++ b/packages/go_router/test/test_helpers.dart @@ -343,6 +343,12 @@ class DummyStatefulWidget extends StatefulWidget { } class DummyStatefulWidgetState extends State { + int counter = 0; + + void increment() => setState(() { + counter++; + }); + @override Widget build(BuildContext context) => Container(); } From 3a00a545c49e81bff48ceb42b32702853619e4b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20Lo=CC=88fstrand?= Date: Fri, 30 Sep 2022 15:48:03 +0200 Subject: [PATCH 017/112] Added more detail --- packages/go_router/CHANGELOG.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/go_router/CHANGELOG.md b/packages/go_router/CHANGELOG.md index 10d7f7f70fb0..cfaeebe6e742 100644 --- a/packages/go_router/CHANGELOG.md +++ b/packages/go_router/CHANGELOG.md @@ -1,7 +1,9 @@ ## 5.1.0 -- Adds support for customising the nested navigation of `ShellRoute`, to support things like - preserving state in nested navigators (flutter/flutter#99124). +- Introduced a new shell route class called `PartitionedShellRoute`, to support using separate + navigators for child routes as well as preserving state in each navigation tree + (flutter/flutter#99124). Also introduced the supporting widget class `StackedNavigationShell`, + which facilitates using an `IndexStack` to manage multiple parallel navigation trees. ## 5.0.3 From b0a626473b69e8afc0517a4add80ec9cfa09e03c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20Lo=CC=88fstrand?= Date: Fri, 30 Sep 2022 16:50:27 +0200 Subject: [PATCH 018/112] Fixed analyzer issue and code style issue. --- packages/go_router/lib/go_router.dart | 2 +- packages/go_router/lib/src/misc/stacked_navigation_shell.dart | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/go_router/lib/go_router.dart b/packages/go_router/lib/go_router.dart index b2fc804c6f08..094aed055907 100644 --- a/packages/go_router/lib/go_router.dart +++ b/packages/go_router/lib/go_router.dart @@ -7,7 +7,7 @@ library go_router; export 'src/configuration.dart' - show GoRoute, GoRouterState, RouteBase, ShellRoute, PartitionedShellRoute; + show GoRoute, GoRouterState, PartitionedShellRoute, RouteBase, ShellRoute; export 'src/misc/extensions.dart'; export 'src/misc/inherited_router.dart'; export 'src/misc/stacked_navigation_shell.dart'; diff --git a/packages/go_router/lib/src/misc/stacked_navigation_shell.dart b/packages/go_router/lib/src/misc/stacked_navigation_shell.dart index 0685019a009b..7b5fd10953e5 100644 --- a/packages/go_router/lib/src/misc/stacked_navigation_shell.dart +++ b/packages/go_router/lib/src/misc/stacked_navigation_shell.dart @@ -58,8 +58,7 @@ class StackedNavigationItemState { /// Gets the current location from the [currentRouterState] or falls back to /// the root route location of the associated [item]. - String get currentLocation => - lastLocation != null ? lastLocation! : item.rootRoutePath; + String get currentLocation => lastLocation ?? item.rootRoutePath; } /// Widget that maintains a stateful stack of [Navigator]s, using an From 3b3909fb8ca8eb0f9a4eb8ad5da8a85b9d3fc4a5 Mon Sep 17 00:00:00 2001 From: John Ryan Date: Fri, 30 Sep 2022 13:28:49 -0700 Subject: [PATCH 019/112] Fix test --- packages/go_router/test/go_router_test.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/go_router/test/go_router_test.dart b/packages/go_router/test/go_router_test.dart index 766729ed5cc4..04ac0e6f0612 100644 --- a/packages/go_router/test/go_router_test.dart +++ b/packages/go_router/test/go_router_test.dart @@ -2545,7 +2545,7 @@ void main() { expect(find.text('Screen B'), findsNothing); expect(find.text('Screen B Detail'), findsOneWidget); - await simulateAndroidBackButton(); + await simulateAndroidBackButton(tester); await tester.pumpAndSettle(); expect(find.text('Screen A'), findsNothing); From eb6d4d379eed448c98e1658aa3d9e6c9c1da3162 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20Lo=CC=88fstrand?= Date: Sun, 2 Oct 2022 16:05:29 +0200 Subject: [PATCH 020/112] Introduced the method `navigatorKeyForChildRoute` on ShellRouteBase to simplify and make the way a navigator is fetched for a shell route more clean. Also removed the need to specify parentNavigatorKey for children of PartitionedShellRoute (navigatorKeys is now the source of truth for navigation keys). Added more asserts to PartitionedShellRoute and updated docs. Renamed field navigationKeys to navigatorKeys on PartitionedShellRoute. --- .../lib/stateful_nested_navigation.dart | 2 - packages/go_router/lib/src/builder.dart | 30 ++++------- packages/go_router/lib/src/configuration.dart | 2 +- packages/go_router/lib/src/delegate.dart | 11 +++- packages/go_router/lib/src/route.dart | 50 ++++++++++++++----- packages/go_router/test/builder_test.dart | 12 ++++- .../go_router/test/configuration_test.dart | 44 +++++++++++++--- 7 files changed, 105 insertions(+), 46 deletions(-) diff --git a/packages/go_router/example/lib/stateful_nested_navigation.dart b/packages/go_router/example/lib/stateful_nested_navigation.dart index a4324a3762e5..9e03a218db5a 100644 --- a/packages/go_router/example/lib/stateful_nested_navigation.dart +++ b/packages/go_router/example/lib/stateful_nested_navigation.dart @@ -75,7 +75,6 @@ class NestedTabNavigationExampleApp extends StatelessWidget { /// navigation bar. Note that the root route must specify the /// `parentNavigatorKey` GoRoute( - parentNavigatorKey: _sectionANavigatorKey, path: '/a', builder: (BuildContext context, GoRouterState state) => const RootScreen(label: 'A', detailsPath: '/a/details'), @@ -94,7 +93,6 @@ class NestedTabNavigationExampleApp extends StatelessWidget { /// The screen to display as the root in the second tab of the bottom /// navigation bar. GoRoute( - parentNavigatorKey: _sectionBNavigatorKey, path: '/b', builder: (BuildContext context, GoRouterState state) => const RootScreen( diff --git a/packages/go_router/lib/src/builder.dart b/packages/go_router/lib/src/builder.dart index 976dc93721ea..6680bd9f8248 100644 --- a/packages/go_router/lib/src/builder.dart +++ b/packages/go_router/lib/src/builder.dart @@ -166,6 +166,10 @@ class RouteBuilder { _buildRecursive(context, matchList, startIndex + 1, pop, routerNeglect, keyToPages, newParams, navigatorKey); } else if (route is ShellRouteBase) { + if (startIndex + 1 >= matchList.matches.length) { + throw _RouteBuilderError('Shell routes must always have child routes'); + } + // The key for the Navigator that will display this ShellRoute's page. final GlobalKey parentNavigatorKey = navigatorKey; @@ -177,26 +181,12 @@ class RouteBuilder { // that the page for this ShellRoute is placed at the right index. final int shellPageIdx = keyToPages[parentNavigatorKey]!.length; - // The key to provide to the ShellRoute's Navigator. - final GlobalKey shellNavigatorKey; - - if (route is PartitionedShellRoute) { - // Get the next route match (sub route), which for a PartitionedShellRoute - // should always be a GoRoute with a parentNavigatorKey - final RouteBase? subRoute = (matchList.matches.length > startIndex + 1) - ? matchList.matches[startIndex + 1].route - : null; - if (subRoute is! GoRoute || subRoute.parentNavigatorKey == null) { - throw _RouteBuilderError( - 'Direct descendants of PartitionedShellRoute ' - '($route) must be GoRoute with a parentNavigatorKey.'); - } - shellNavigatorKey = subRoute.parentNavigatorKey!; - } else if (route is ShellRoute) { - shellNavigatorKey = route.navigatorKey; - } else { - throw _RouteBuilderError('Unknown route type: $route'); - } + // Get the current child route of this shell route from the match list. + final RouteBase childRoute = matchList.matches[startIndex + 1].route; + + // The key to provide to the shell route's Navigator. + final GlobalKey shellNavigatorKey = + route.navigatorKeyForChildRoute(childRoute); // Add an entry for the shell route's navigator keyToPages.putIfAbsent(shellNavigatorKey, () => >[]); diff --git a/packages/go_router/lib/src/configuration.dart b/packages/go_router/lib/src/configuration.dart index 06515b18c86b..fddcfebe1c51 100644 --- a/packages/go_router/lib/src/configuration.dart +++ b/packages/go_router/lib/src/configuration.dart @@ -85,7 +85,7 @@ class RouteConfiguration { route.routes, >[ ...allowedKeys, - ...route.navigationKeys, + ...route.navigatorKeys, ], ); } diff --git a/packages/go_router/lib/src/delegate.dart b/packages/go_router/lib/src/delegate.dart index a51b953340b1..e33c4d3d460d 100644 --- a/packages/go_router/lib/src/delegate.dart +++ b/packages/go_router/lib/src/delegate.dart @@ -118,6 +118,7 @@ class GoRouterDelegate extends RouterDelegate bool canPop() { // Loop through navigators in reverse and call canPop() final int matchCount = _matchList.matches.length; + RouteBase? childRoute; for (int i = matchCount - 1; i >= 0; i -= 1) { final RouteMatch match = _matchList.matches[i]; final RouteBase route = match.route; @@ -128,14 +129,20 @@ class GoRouterDelegate extends RouterDelegate if (canPop) { return canPop; } - } else if (route is ShellRoute) { - final bool canPop = route.navigatorKey.currentState!.canPop(); + } else if (route is ShellRouteBase && childRoute != null) { + // For shell routes, find the navigator key that should be used for the + // child route in the current match list + final GlobalKey navigatorKey = + route.navigatorKeyForChildRoute(childRoute); + + final bool canPop = navigatorKey.currentState!.canPop(); // Continue if canPop is false. if (canPop) { return canPop; } } + childRoute = route; } return navigatorKey.currentState?.canPop() ?? false; } diff --git a/packages/go_router/lib/src/route.dart b/packages/go_router/lib/src/route.dart index ab1f218cbcee..a37787b0914d 100644 --- a/packages/go_router/lib/src/route.dart +++ b/packages/go_router/lib/src/route.dart @@ -6,6 +6,7 @@ import 'package:collection/collection.dart'; import 'package:flutter/widgets.dart'; import 'configuration.dart'; +import 'misc/errors.dart'; import 'misc/stacked_navigation_shell.dart'; import 'pages/custom_transition_page.dart'; import 'path_utils.dart'; @@ -353,6 +354,10 @@ abstract class ShellRouteBase extends RouteBase { /// This child parameter is the Widget built by calling the matching /// sub-route's builder. final ShellRoutePageBuilder? pageBuilder; + + /// Returns the key for the [Navigator] that is to be used for the specified + /// child route. + GlobalKey navigatorKeyForChildRoute(RouteBase route); } /// A route that displays a UI shell around the matching child route. @@ -459,7 +464,6 @@ class ShellRoute extends ShellRouteBase { GlobalKey? navigatorKey, }) : assert(routes.isNotEmpty), navigatorKey = navigatorKey ?? GlobalKey(), - //nestedNavigationBuilder = null, super._() { for (final RouteBase route in routes) { if (route is GoRoute) { @@ -473,6 +477,11 @@ class ShellRoute extends ShellRouteBase { /// All ShellRoutes build a Navigator by default. Child GoRoutes /// are placed onto this Navigator instead of the root Navigator. final GlobalKey navigatorKey; + + @override + GlobalKey navigatorKeyForChildRoute(RouteBase route) { + return navigatorKey; + } } /// A route that displays a UI shell with separate [Navigator]s for its child @@ -481,28 +490,33 @@ class ShellRoute extends ShellRouteBase { /// When using this route class as a parent shell route, it possible to build /// a stateful nested navigation. This is convenient when for instance /// implementing a UI with a [BottomNavigationBar], with a persistent navigation -/// state for each tab +/// state for each tab. /// /// See [Stateful Nested Navigation](https://github.com/flutter/packages/blob/main/packages/go_router/example/lib/stateful_nested_navigation.dart) /// for a complete runnable example. class PartitionedShellRoute extends ShellRouteBase { - /// Constructs a [PartitionedShellRoute] from a list of [GoRoute], that will - /// represent the roots in a stacked navigation hierarchy. For each root route, - /// a separate [Navigator] will be created, using the navigation key specified - /// by the `parentNavigatorKey` property of [GoRoute]. This navigation key must - /// be one of the keys specified in [navigationKeys]. + /// Constructs a [PartitionedShellRoute] from a list of routes, each + /// representing the root of a navigation hierarchy. A separate + /// [Navigator] will be created for each of the root routes, using the + /// navigator key at the corresponding index in [navigatorKeys]. PartitionedShellRoute({ - required List routes, - required this.navigationKeys, + required List routes, + required this.navigatorKeys, super.builder, super.pageBuilder, }) : assert(routes.isNotEmpty), - assert(navigationKeys.length == routes.length), + assert(navigatorKeys.length == routes.length), + assert(Set>.from(navigatorKeys).length == + routes.length), assert(pageBuilder != null || builder != null, 'builder or pageBuilder must be provided'), super._(routes: routes) { for (int i = 0; i < routes.length; ++i) { - assert(routes[i].parentNavigatorKey == navigationKeys[i]); + final RouteBase route = routes[i]; + if (route is GoRoute) { + assert(route.parentNavigatorKey == null || + route.parentNavigatorKey == navigatorKeys[i]); + } } } @@ -522,7 +536,7 @@ class PartitionedShellRoute extends ShellRouteBase { }) { return PartitionedShellRoute( routes: routes, - navigationKeys: stackItems + navigatorKeys: stackItems .map((StackedNavigationItem e) => e.navigatorKey) .toList(), builder: (BuildContext context, GoRouterState state, @@ -539,5 +553,15 @@ class PartitionedShellRoute extends ShellRouteBase { } /// The navigator keys of the navigators created by this route. - final List> navigationKeys; + final List> navigatorKeys; + + @override + GlobalKey navigatorKeyForChildRoute(RouteBase route) { + final int routeIndex = routes.indexOf(route); + if (routeIndex < 0) { + throw GoError('Route $route is not a child of this ' + 'PartitionedShellRoute $this'); + } + return navigatorKeys[routeIndex]; + } } diff --git a/packages/go_router/test/builder_test.dart b/packages/go_router/test/builder_test.dart index 8d6fb75a3a2e..aeecfbb8f149 100644 --- a/packages/go_router/test/builder_test.dart +++ b/packages/go_router/test/builder_test.dart @@ -78,6 +78,16 @@ void main() { final RouteMatchList matches = RouteMatchList([ RouteMatch( route: config.routes.first, + subloc: '', + fullpath: '', + encodedParams: {}, + queryParams: {}, + queryParametersAll: >{}, + extra: null, + error: null, + ), + RouteMatch( + route: config.routes.first.routes.first, subloc: '/', fullpath: '/', encodedParams: {}, @@ -107,7 +117,7 @@ void main() { builder: (BuildContext context, GoRouterState state, Widget child) => child, - navigationKeys: >[ + navigatorKeys: >[ key, ], routes: [ diff --git a/packages/go_router/test/configuration_test.dart b/packages/go_router/test/configuration_test.dart index 0a7f2357bb89..2629c24f0638 100644 --- a/packages/go_router/test/configuration_test.dart +++ b/packages/go_router/test/configuration_test.dart @@ -81,14 +81,44 @@ void main() { }); test( - 'throws when a child of PartitionedShellRoute is missing a ' - 'parentNavigatorKey', () { + 'throws when PartitionedShellRoute is missing a navigator key for a ' + 'child route', () { + final GlobalKey root = + GlobalKey(debugLabel: 'root'); + final GlobalKey keyA = + GlobalKey(debugLabel: 'A'); + final List shellRouteChildren = [ + GoRoute(path: '/a', builder: _mockScreenBuilder), + GoRoute(path: '/b', builder: _mockScreenBuilder), + ]; + expect( + () { + RouteConfiguration( + navigatorKey: root, + routes: [ + PartitionedShellRoute( + routes: shellRouteChildren, + navigatorKeys: >[keyA], + builder: _mockShellBuilder), + ], + redirectLimit: 10, + topRedirect: (BuildContext context, GoRouterState state) { + return null; + }, + ); + }, + throwsAssertionError, + ); + }); + + test('throws when PartitionedShellRoute has duplicate navigator keys', () { final GlobalKey root = GlobalKey(debugLabel: 'root'); - final GlobalKey nested = - GlobalKey(debugLabel: 'nested'); + final GlobalKey keyA = + GlobalKey(debugLabel: 'A'); final List shellRouteChildren = [ - GoRoute(path: '/', builder: _mockScreenBuilder), + GoRoute(path: '/a', builder: _mockScreenBuilder), + GoRoute(path: '/b', builder: _mockScreenBuilder), ]; expect( () { @@ -97,7 +127,7 @@ void main() { routes: [ PartitionedShellRoute( routes: shellRouteChildren, - navigationKeys: >[nested], + navigatorKeys: >[keyA, keyA], builder: _mockShellBuilder), ], redirectLimit: 10, @@ -136,7 +166,7 @@ void main() { routes: [ PartitionedShellRoute( routes: shellRouteChildren, - navigationKeys: >[ + navigatorKeys: >[ sectionANavigatorKey, sectionBNavigatorKey ], From 016be7611dd4ecde709cf0e70ca8c132f6c97294 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20Lo=CC=88fstrand?= Date: Sun, 2 Oct 2022 17:07:33 +0200 Subject: [PATCH 021/112] Added a canPop test for PartitionedShellRoute (by replacing a duplicated test for ShellRoute). --- packages/go_router/test/go_router_test.dart | 62 +++++++++++++-------- 1 file changed, 39 insertions(+), 23 deletions(-) diff --git a/packages/go_router/test/go_router_test.dart b/packages/go_router/test/go_router_test.dart index 04ac0e6f0612..2fd0ae357762 100644 --- a/packages/go_router/test/go_router_test.dart +++ b/packages/go_router/test/go_router_test.dart @@ -2722,15 +2722,23 @@ void main() { ); testWidgets( - 'It checks if ShellRoute navigators can pop', + 'It checks if PartitionedShellRoute navigators can pop', (WidgetTester tester) async { - final GlobalKey shellNavigatorKey = + final GlobalKey rootNavigatorKey = + GlobalKey(); + final GlobalKey shellNavigatorKeyA = + GlobalKey(); + final GlobalKey shellNavigatorKeyB = GlobalKey(); final GoRouter router = GoRouter( + navigatorKey: rootNavigatorKey, initialLocation: '/a', routes: [ - ShellRoute( - navigatorKey: shellNavigatorKey, + PartitionedShellRoute( + navigatorKeys: >[ + shellNavigatorKeyA, + shellNavigatorKeyB + ], builder: (BuildContext context, GoRouterState state, Widget child) { return Scaffold( @@ -2742,24 +2750,29 @@ void main() { GoRoute( path: '/a', builder: (BuildContext context, _) { - return Scaffold( - body: TextButton( - onPressed: () async { - shellNavigatorKey.currentState!.push( - MaterialPageRoute( - builder: (BuildContext context) { - return const Scaffold( - body: Text('pageless route'), - ); - }, - ), - ); - }, - child: const Text('Push'), - ), + return const Scaffold( + body: Text('Screen A'), ); }, ), + GoRoute( + path: '/b', + builder: (BuildContext context, _) { + return const Scaffold( + body: Text('Screen B'), + ); + }, + routes: [ + GoRoute( + path: 'detail', + builder: (BuildContext context, _) { + return const Scaffold( + body: Text('Screen B detail'), + ); + }, + ), + ], + ), ], ), ], @@ -2773,17 +2786,20 @@ void main() { ); expect(router.canPop(), false); - expect(find.text('Push'), findsOneWidget); - await tester.tap(find.text('Push')); + router.go('/b/detail'); await tester.pumpAndSettle(); - expect( - find.text('pageless route', skipOffstage: false), findsOneWidget); + expect(find.text('Screen B detail', skipOffstage: false), + findsOneWidget); expect(router.canPop(), true); + // Verify that it is actually the PartitionedShellRoute that reports + // canPop = true + expect(rootNavigatorKey.currentState?.canPop(), false); }, ); }); + group('pop', () { testWidgets( 'Should pop from the correct navigator when parentNavigatorKey is set', From 4ae26d7581f1046f330006c3cbe9e11f5e7e1744 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20Lo=CC=88fstrand?= Date: Sun, 2 Oct 2022 17:25:51 +0200 Subject: [PATCH 022/112] Updated implementation of popRoute be in sync with canPop. --- packages/go_router/lib/src/delegate.dart | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/packages/go_router/lib/src/delegate.dart b/packages/go_router/lib/src/delegate.dart index e33c4d3d460d..498802cd0276 100644 --- a/packages/go_router/lib/src/delegate.dart +++ b/packages/go_router/lib/src/delegate.dart @@ -54,6 +54,7 @@ class GoRouterDelegate extends RouterDelegate // a non-null parentNavigatorKey or a ShellRoute with a non-null // parentNavigatorKey and pop from that Navigator instead of the root. final int matchCount = _matchList.matches.length; + RouteBase? childRoute; for (int i = matchCount - 1; i >= 0; i -= 1) { final RouteMatch match = _matchList.matches[i]; final RouteBase route = match.route; @@ -66,14 +67,20 @@ class GoRouterDelegate extends RouterDelegate if (didPop) { return didPop; } - } else if (route is ShellRoute) { - final bool didPop = await route.navigatorKey.currentState!.maybePop(); + } else if (route is ShellRouteBase && childRoute != null) { + // For shell routes, find the navigator key that should be used for the + // child route in the current match list + final GlobalKey navigatorKey = + route.navigatorKeyForChildRoute(childRoute); + + final bool didPop = await navigatorKey.currentState!.maybePop(); // Continue if didPop was false. if (didPop) { return didPop; } } + childRoute = route; } // Use the root navigator if no ShellRoute Navigators were found and didn't From 1f3f01abf83f08bd3fded00aaf3e1e0f9a82369f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20Lo=CC=88fstrand?= Date: Mon, 3 Oct 2022 13:31:28 +0200 Subject: [PATCH 023/112] Updated documentation of PartitionedShellRoute with examples. Minor changes to replace use of GoRoute with RouteBase in collections. --- .../lib/stateful_nested_navigation.dart | 5 +- packages/go_router/lib/src/route.dart | 124 ++++++++++++++++-- 2 files changed, 117 insertions(+), 12 deletions(-) diff --git a/packages/go_router/example/lib/stateful_nested_navigation.dart b/packages/go_router/example/lib/stateful_nested_navigation.dart index 9e03a218db5a..6ca8dce7f7a4 100644 --- a/packages/go_router/example/lib/stateful_nested_navigation.dart +++ b/packages/go_router/example/lib/stateful_nested_navigation.dart @@ -70,10 +70,9 @@ class NestedTabNavigationExampleApp extends StatelessWidget { transitionBuilder: (BuildContext context, Animation animation, Widget child) => FadeTransition(opacity: animation, child: child), - routes: [ + routes: [ /// The screen to display as the root in the first tab of the bottom - /// navigation bar. Note that the root route must specify the - /// `parentNavigatorKey` + /// navigation bar. GoRoute( path: '/a', builder: (BuildContext context, GoRouterState state) => diff --git a/packages/go_router/lib/src/route.dart b/packages/go_router/lib/src/route.dart index a37787b0914d..e93a461ca4c0 100644 --- a/packages/go_router/lib/src/route.dart +++ b/packages/go_router/lib/src/route.dart @@ -487,10 +487,116 @@ class ShellRoute extends ShellRouteBase { /// A route that displays a UI shell with separate [Navigator]s for its child /// routes. /// -/// When using this route class as a parent shell route, it possible to build -/// a stateful nested navigation. This is convenient when for instance -/// implementing a UI with a [BottomNavigationBar], with a persistent navigation -/// state for each tab. +/// Similar to [ShellRoute], this route class places its sub routes on a +/// separate Navigator instead of the root Navigator. However, this route +/// class differs in that it uses a separate Navigator for each of its sub +/// route trees, making it possible to build a stateful nested navigation. This +/// is convenient when for instance implementing a UI with a +/// [BottomNavigationBar], with a persistent navigation state for each tab. +/// +/// Below is a simple example of how a router configuration with +/// PartitionedShellRoute could be achieved. In this example, a +/// BottomNavigationBar with two tabs is used, and each of the tabs gets its +/// own Navigator. This Navigator will then be passed as the child argument +/// of the [builder] function. +/// +/// ``` +/// final GlobalKey _tabANavigatorKey = +/// GlobalKey(debugLabel: 'tabANavigator'); +/// final GlobalKey _tabBNavigatorKey = +/// GlobalKey(debugLabel: 'tabBNavigator'); +/// +/// final GoRouter _router = GoRouter( +/// initialLocation: '/a', +/// routes: [ +/// PartitionedShellRoute( +/// navigatorKeys: >[ +/// _tabANavigatorKey, _tabBNavigatorKey, +/// ], +/// builder: (BuildContext context, GoRouterState state, Widget child) { +/// return CustomScaffoldWithBottomNavigationBar( +/// currentNavigator: child as Navigator, +/// currentRouterState: state, +/// ); +/// }, +/// routes: [ +/// /// The root of tab 'A' +/// GoRoute( +/// path: '/a', +/// builder: (BuildContext context, GoRouterState state) => +/// const RootScreen(label: 'A', detailsPath: '/a/details'), +/// routes: [ +/// /// Will cover screen A but not the bottom navigation bar +/// GoRoute( +/// path: 'details', +/// builder: (BuildContext context, GoRouterState state) => +/// const DetailsScreen(label: 'A'), +/// ), +/// ], +/// ), +/// +/// /// The root of tab 'B' +/// GoRoute( +/// path: '/b', +/// builder: (BuildContext context, GoRouterState state) => +/// const RootScreen(label: 'B', detailsPath: '/b/details/1'), +/// routes: [ +/// /// Will cover screen B but not the bottom navigation bar +/// GoRoute( +/// path: 'details', +/// builder: (BuildContext context, GoRouterState state) => +/// const DetailsScreen(label: 'B'), +/// ), +/// ], +/// ), +/// ], +/// ), +/// ], +/// ); +/// ``` +/// +/// For scenarios where it's suitable to use an [IndexStack] to manage the +/// navigators, consider using the [PartitionedShellRoute.stackedNavigation] +/// constructor instead to reduce boilerplate. For example: +/// +/// ``` +/// final GoRouter _router = GoRouter( +/// initialLocation: '/a', +/// routes: [ +/// PartitionedShellRoute.stackedNavigation( +/// stackItems: [ +/// StackedNavigationItem( +/// rootRoutePath: '/a', navigatorKey: _sectionANavigatorKey), +/// StackedNavigationItem( +/// rootRoutePath: '/b', navigatorKey: _sectionBNavigatorKey), +/// ], +/// scaffoldBuilder: (BuildContext context, int currentIndex, +/// List itemsState, +/// Widget scaffoldBody) { +/// return ScaffoldWithBottomNavigationBar( +/// currentIndex: currentIndex, +/// itemsState: itemsState, +/// body: scaffoldBody, +/// ); +/// }, +/// routes: [ +/// /// The root of tab 'A' +/// GoRoute( +/// path: '/a', +/// builder: (BuildContext context, GoRouterState state) => +/// const RootScreen(label: 'A', detailsPath: '/a/details'), +/// ), +/// /// The root of tab 'B' +/// GoRoute( +/// path: '/b', +/// builder: (BuildContext context, GoRouterState state) => +/// const RootScreen(label: 'B', detailsPath: '/b/details/1'), +/// ), +/// ], +/// ), +/// ], +/// ); +/// ``` /// /// See [Stateful Nested Navigation](https://github.com/flutter/packages/blob/main/packages/go_router/example/lib/stateful_nested_navigation.dart) /// for a complete runnable example. @@ -525,10 +631,10 @@ class PartitionedShellRoute extends ShellRouteBase { /// /// Each route in the `routes` parameter must correspond to a /// [StackedNavigationItem], specified in the `stackItems` parameter. - /// The stacked navigation shell can be customized by specifying a + /// The stacked navigation shell can be implemented by specifying a /// `scaffoldBuilder`, to build a widget that wraps the index stack. factory PartitionedShellRoute.stackedNavigation({ - required List routes, + required List routes, required List stackItems, StackedNavigationScaffoldBuilder? scaffoldBuilder, StackedNavigationTransitionBuilder? transitionBuilder, @@ -539,10 +645,10 @@ class PartitionedShellRoute extends ShellRouteBase { navigatorKeys: stackItems .map((StackedNavigationItem e) => e.navigatorKey) .toList(), - builder: (BuildContext context, GoRouterState state, - Widget currentTabNavigator) { + builder: (BuildContext context, GoRouterState state, Widget child) { + assert(child is Navigator); return StackedNavigationShell( - currentNavigator: currentTabNavigator as Navigator, + currentNavigator: child as Navigator, currentRouterState: state, stackItems: stackItems, scaffoldBuilder: scaffoldBuilder, From 3b386410a1b09791d20a2d2f6ea09048b1f75a92 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20Lo=CC=88fstrand?= Date: Mon, 3 Oct 2022 17:25:34 +0200 Subject: [PATCH 024/112] Updated documentation for builder and pageBuilder fields of ShellRouteBase, to more correctly describe meaning of the child argument in the builder functions. --- packages/go_router/CHANGELOG.md | 2 ++ packages/go_router/lib/src/route.dart | 8 ++++---- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/packages/go_router/CHANGELOG.md b/packages/go_router/CHANGELOG.md index b3724b510d39..46e34728fbcb 100644 --- a/packages/go_router/CHANGELOG.md +++ b/packages/go_router/CHANGELOG.md @@ -4,6 +4,8 @@ navigators for child routes as well as preserving state in each navigation tree (flutter/flutter#99124). Also introduced the supporting widget class `StackedNavigationShell`, which facilitates using an `IndexStack` to manage multiple parallel navigation trees. +- Updated documentation for `pageBuilder` and `builder` fields of `ShellRoute`, to more correctly + describe the meaning of the child argument in the builder functions. ## 5.0.5 diff --git a/packages/go_router/lib/src/route.dart b/packages/go_router/lib/src/route.dart index e93a461ca4c0..e838ff2150dd 100644 --- a/packages/go_router/lib/src/route.dart +++ b/packages/go_router/lib/src/route.dart @@ -344,15 +344,15 @@ abstract class ShellRouteBase extends RouteBase { /// The widget builder for a shell route. /// /// Similar to GoRoute builder, but with an additional child parameter. This - /// child parameter is the Widget built by calling the matching sub-route's - /// builder. + /// child parameter is the [Navigator] that will be used for the matching + /// sub-routes. final ShellRouteBuilder? builder; /// The page builder for a shell route. /// /// Similar to GoRoute pageBuilder, but with an additional child parameter. - /// This child parameter is the Widget built by calling the matching - /// sub-route's builder. + /// This child parameter is the [Navigator] that will be used for the matching + /// sub-routes. final ShellRoutePageBuilder? pageBuilder; /// Returns the key for the [Navigator] that is to be used for the specified From d701ab52993e3fb5a1cb16f568ec1c06bf3314f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20Lo=CC=88fstrand?= Date: Mon, 3 Oct 2022 17:36:47 +0200 Subject: [PATCH 025/112] Fixed documentation typos and minor refactoring (renaming). --- .../example/lib/stateful_nested_navigation.dart | 14 +++++++------- .../lib/src/misc/stacked_navigation_shell.dart | 2 +- packages/go_router/lib/src/route.dart | 14 +++++++------- packages/go_router/test/go_router_test.dart | 4 ++-- 4 files changed, 17 insertions(+), 17 deletions(-) diff --git a/packages/go_router/example/lib/stateful_nested_navigation.dart b/packages/go_router/example/lib/stateful_nested_navigation.dart index 6ca8dce7f7a4..c60dfaaa0b54 100644 --- a/packages/go_router/example/lib/stateful_nested_navigation.dart +++ b/packages/go_router/example/lib/stateful_nested_navigation.dart @@ -5,10 +5,10 @@ import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; -final GlobalKey _sectionANavigatorKey = - GlobalKey(debugLabel: 'sectionANav'); -final GlobalKey _sectionBNavigatorKey = - GlobalKey(debugLabel: 'sectionBNav'); +final GlobalKey _tabANavigatorKey = + GlobalKey(debugLabel: 'tabANav'); +final GlobalKey _tabBNavigatorKey = + GlobalKey(debugLabel: 'tabBNav'); // This example demonstrates how to setup nested navigation using a // BottomNavigationBar, where each tab uses its own persistent navigator, i.e. @@ -35,12 +35,12 @@ class NestedTabNavigationExampleApp extends StatelessWidget { [ ScaffoldWithNavBarTabItem( navigationItem: StackedNavigationItem( - rootRoutePath: '/a', navigatorKey: _sectionANavigatorKey), + rootRoutePath: '/a', navigatorKey: _tabANavigatorKey), icon: const Icon(Icons.home), label: 'Section A'), ScaffoldWithNavBarTabItem( navigationItem: StackedNavigationItem( - rootRoutePath: '/b', navigatorKey: _sectionBNavigatorKey), + rootRoutePath: '/b', navigatorKey: _tabBNavigatorKey), icon: const Icon(Icons.settings), label: 'Section B', ), @@ -52,7 +52,7 @@ class NestedTabNavigationExampleApp extends StatelessWidget { /// Custom top shell route - wraps the below routes in a scaffold with /// a bottom tab navigator (ScaffoldWithNavBar). Each tab will use its own /// Navigator, provided by MultiPathShellRoute. - PartitionedShellRoute.stackedNavigation( + PartitionedShellRoute.stackedNavigationShell( stackItems: _tabs .map((ScaffoldWithNavBarTabItem e) => e.navigationItem) .toList(), diff --git a/packages/go_router/lib/src/misc/stacked_navigation_shell.dart b/packages/go_router/lib/src/misc/stacked_navigation_shell.dart index 7b5fd10953e5..3232a5106608 100644 --- a/packages/go_router/lib/src/misc/stacked_navigation_shell.dart +++ b/packages/go_router/lib/src/misc/stacked_navigation_shell.dart @@ -62,7 +62,7 @@ class StackedNavigationItemState { } /// Widget that maintains a stateful stack of [Navigator]s, using an -/// [IndexStack]. +/// [IndexedStack]. /// /// Each item in the stack is represented by a [StackedNavigationItem], /// specified in the `stackItems` parameter. The stack items will be used to diff --git a/packages/go_router/lib/src/route.dart b/packages/go_router/lib/src/route.dart index e838ff2150dd..6e749b109c23 100644 --- a/packages/go_router/lib/src/route.dart +++ b/packages/go_router/lib/src/route.dart @@ -555,8 +555,8 @@ class ShellRoute extends ShellRouteBase { /// ); /// ``` /// -/// For scenarios where it's suitable to use an [IndexStack] to manage the -/// navigators, consider using the [PartitionedShellRoute.stackedNavigation] +/// For scenarios where it's suitable to use an [IndexedStack] to manage the +/// navigators, consider using the [PartitionedShellRoute.stackedNavigationShell] /// constructor instead to reduce boilerplate. For example: /// /// ``` @@ -566,9 +566,9 @@ class ShellRoute extends ShellRouteBase { /// PartitionedShellRoute.stackedNavigation( /// stackItems: [ /// StackedNavigationItem( -/// rootRoutePath: '/a', navigatorKey: _sectionANavigatorKey), +/// rootRoutePath: '/a', navigatorKey: _tabANavigatorKey), /// StackedNavigationItem( -/// rootRoutePath: '/b', navigatorKey: _sectionBNavigatorKey), +/// rootRoutePath: '/b', navigatorKey: _tabBNavigatorKey), /// ], /// scaffoldBuilder: (BuildContext context, int currentIndex, /// List itemsState, @@ -627,13 +627,13 @@ class PartitionedShellRoute extends ShellRouteBase { } /// Constructs a [PartitionedShellRoute] that places its navigators in an - /// [IndexStack], managed by a [StackedNavigationShell]. + /// [IndexedStack], managed by a [StackedNavigationShell]. /// /// Each route in the `routes` parameter must correspond to a /// [StackedNavigationItem], specified in the `stackItems` parameter. /// The stacked navigation shell can be implemented by specifying a - /// `scaffoldBuilder`, to build a widget that wraps the index stack. - factory PartitionedShellRoute.stackedNavigation({ + /// `scaffoldBuilder`, to build a widget that wraps the indexed stack. + factory PartitionedShellRoute.stackedNavigationShell({ required List routes, required List stackItems, StackedNavigationScaffoldBuilder? scaffoldBuilder, diff --git a/packages/go_router/test/go_router_test.dart b/packages/go_router/test/go_router_test.dart index 2fd0ae357762..a141d24519c8 100644 --- a/packages/go_router/test/go_router_test.dart +++ b/packages/go_router/test/go_router_test.dart @@ -2424,7 +2424,7 @@ void main() { ]; final List routes = [ - PartitionedShellRoute.stackedNavigation( + PartitionedShellRoute.stackedNavigationShell( stackItems: stackItems, routes: [ GoRoute( @@ -2497,7 +2497,7 @@ void main() { ]; final List routes = [ - PartitionedShellRoute.stackedNavigation( + PartitionedShellRoute.stackedNavigationShell( stackItems: stackItems, routes: [ GoRoute( From bda571b0efa5852407e3dd7213d6996ae61bb035 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20Lo=CC=88fstrand?= Date: Fri, 7 Oct 2022 16:31:32 +0200 Subject: [PATCH 026/112] Refactored PartitionedShellRoute and renamed to StatefulShellRoute. StatefulShellRoute now always creates a Widget (StatefulNavigationShell) that manages then nested stateful navigators, and switching between them. Details around usage of IndexedStack for managing the stateful navigators are now also hidden. --- .../lib/stateful_nested_navigation.dart | 102 ++----- packages/go_router/lib/go_router.dart | 19 +- packages/go_router/lib/src/builder.dart | 88 +++++- packages/go_router/lib/src/configuration.dart | 27 +- packages/go_router/lib/src/delegate.dart | 9 +- .../src/misc/stacked_navigation_shell.dart | 221 -------------- .../src/misc/stateful_navigation_shell.dart | 214 +++++++++++++ packages/go_router/lib/src/route.dart | 288 +++++++++++------- packages/go_router/lib/src/state.dart | 57 +++- packages/go_router/test/builder_test.dart | 7 +- .../go_router/test/configuration_test.dart | 143 +++++++-- packages/go_router/test/go_router_test.dart | 40 +-- 12 files changed, 725 insertions(+), 490 deletions(-) delete mode 100644 packages/go_router/lib/src/misc/stacked_navigation_shell.dart create mode 100644 packages/go_router/lib/src/misc/stateful_navigation_shell.dart diff --git a/packages/go_router/example/lib/stateful_nested_navigation.dart b/packages/go_router/example/lib/stateful_nested_navigation.dart index c60dfaaa0b54..42c82fb2daa5 100644 --- a/packages/go_router/example/lib/stateful_nested_navigation.dart +++ b/packages/go_router/example/lib/stateful_nested_navigation.dart @@ -31,49 +31,30 @@ class NestedTabNavigationExampleApp extends StatelessWidget { /// Creates a NestedTabNavigationExampleApp NestedTabNavigationExampleApp({Key? key}) : super(key: key); - static final List _tabs = - [ - ScaffoldWithNavBarTabItem( - navigationItem: StackedNavigationItem( - rootRoutePath: '/a', navigatorKey: _tabANavigatorKey), - icon: const Icon(Icons.home), - label: 'Section A'), - ScaffoldWithNavBarTabItem( - navigationItem: StackedNavigationItem( - rootRoutePath: '/b', navigatorKey: _tabBNavigatorKey), - icon: const Icon(Icons.settings), - label: 'Section B', - ), - ]; - final GoRouter _router = GoRouter( initialLocation: '/a', routes: [ /// Custom top shell route - wraps the below routes in a scaffold with /// a bottom tab navigator (ScaffoldWithNavBar). Each tab will use its own - /// Navigator, provided by MultiPathShellRoute. - PartitionedShellRoute.stackedNavigationShell( - stackItems: _tabs - .map((ScaffoldWithNavBarTabItem e) => e.navigationItem) - .toList(), - scaffoldBuilder: (BuildContext context, int currentIndex, - List itemsState, Widget scaffoldBody) { - return ScaffoldWithNavBar( - tabs: _tabs, - currentIndex: currentIndex, - itemsState: itemsState, - body: scaffoldBody); + /// Navigator, provided by StatefulShellRoute. + StatefulShellRoute.navigationBranchRoutes( + builder: (BuildContext context, GoRouterState state, + Widget statefulShellNavigation) { + return ScaffoldWithNavBar(body: statefulShellNavigation); }, - - /// A transition builder is optional, only included here for - /// demonstration purposes. - transitionBuilder: - (BuildContext context, Animation animation, Widget child) => - FadeTransition(opacity: animation, child: child), - routes: [ + pageProvider: + (BuildContext context, GoRouterState state, Widget statefulShell) { + return NoTransitionPage(child: statefulShell); + }, + // A transition builder is optional: + // transitionBuilder: + // (BuildContext context, Animation animation, Widget child) => + // FadeTransition(opacity: animation, child: child), + routes: [ /// The screen to display as the root in the first tab of the bottom /// navigation bar. GoRoute( + parentNavigatorKey: _tabANavigatorKey, path: '/a', builder: (BuildContext context, GoRouterState state) => const RootScreen(label: 'A', detailsPath: '/a/details'), @@ -82,16 +63,16 @@ class NestedTabNavigationExampleApp extends StatelessWidget { /// first tab. This will cover screen A but not the application /// shell (bottom navigation bar). GoRoute( - path: 'details', - builder: (BuildContext context, GoRouterState state) => - const DetailsScreen(label: 'A'), - ), + path: 'details', + builder: (BuildContext context, GoRouterState state) => + const DetailsScreen(label: 'A')), ], ), /// The screen to display as the root in the second tab of the bottom /// navigation bar. GoRoute( + parentNavigatorKey: _tabBNavigatorKey, path: '/b', builder: (BuildContext context, GoRouterState state) => const RootScreen( @@ -126,60 +107,41 @@ class NestedTabNavigationExampleApp extends StatelessWidget { } } -/// Representation of a tab item in a [ScaffoldWithNavBar] -class ScaffoldWithNavBarTabItem extends BottomNavigationBarItem { - /// Constructs an [ScaffoldWithNavBarTabItem]. - const ScaffoldWithNavBarTabItem( - {required this.navigationItem, required Widget icon, String? label}) - : super(icon: icon, label: label); - - /// The [StackedNavigationItem] - final StackedNavigationItem navigationItem; - - /// Gets the associated navigator key - GlobalKey get navigatorKey => navigationItem.navigatorKey; -} - /// Builds the "shell" for the app by building a Scaffold with a /// BottomNavigationBar, where [child] is placed in the body of the Scaffold. class ScaffoldWithNavBar extends StatelessWidget { /// Constructs an [ScaffoldWithNavBar]. const ScaffoldWithNavBar({ - required this.currentIndex, - required this.itemsState, required this.body, - required this.tabs, Key? key, }) : super(key: key ?? const ValueKey('ScaffoldWithNavBar')); - /// Currently active tab index - final int currentIndex; - - /// Route state - final List itemsState; - /// Body, i.e. the index stack final Widget body; - /// The tabs - final List tabs; - @override Widget build(BuildContext context) { + final StatefulShellRouteState shellState = StatefulShellRoute.of(context); return Scaffold( body: body, bottomNavigationBar: BottomNavigationBar( - items: tabs, - currentIndex: currentIndex, - onTap: (int tappedIndex) => - _onItemTapped(context, itemsState[tappedIndex]), + items: const [ + BottomNavigationBarItem(icon: Icon(Icons.home), label: 'Section A'), + BottomNavigationBarItem( + icon: Icon(Icons.settings), label: 'Section B'), + ], + currentIndex: shellState.currentBranchIndex, + onTap: (int tappedIndex) => _onItemTapped( + context, + shellState.navigationBranchState[tappedIndex], + ), ), ); } void _onItemTapped( - BuildContext context, StackedNavigationItemState itemState) { - GoRouter.of(context).go(itemState.currentLocation); + BuildContext context, ShellNavigationBranchState routeState) { + GoRouter.of(context).go(routeState.currentLocation); } } diff --git a/packages/go_router/lib/go_router.dart b/packages/go_router/lib/go_router.dart index 094aed055907..2a9205f99d43 100644 --- a/packages/go_router/lib/go_router.dart +++ b/packages/go_router/lib/go_router.dart @@ -7,12 +7,25 @@ library go_router; export 'src/configuration.dart' - show GoRoute, GoRouterState, PartitionedShellRoute, RouteBase, ShellRoute; + show + GoRoute, + GoRouterState, + RouteBase, + ShellNavigationBranchItem, + ShellNavigationBranchState, + ShellRoute, + StatefulShellRoute, + StatefulShellRouteState; export 'src/misc/extensions.dart'; export 'src/misc/inherited_router.dart'; -export 'src/misc/stacked_navigation_shell.dart'; export 'src/pages/custom_transition_page.dart'; export 'src/platform.dart' show UrlPathStrategy; export 'src/route_data.dart' show GoRouteData, TypedGoRoute; export 'src/router.dart'; -export 'src/typedefs.dart' show GoRouterPageBuilder, GoRouterRedirect; +export 'src/typedefs.dart' + show + GoRouterPageBuilder, + GoRouterRedirect, + GoRouterWidgetBuilder, + ShellRouteBuilder, + ShellRoutePageBuilder; diff --git a/packages/go_router/lib/src/builder.dart b/packages/go_router/lib/src/builder.dart index 6680bd9f8248..703fe1f233fc 100644 --- a/packages/go_router/lib/src/builder.dart +++ b/packages/go_router/lib/src/builder.dart @@ -9,6 +9,7 @@ import 'logging.dart'; import 'match.dart'; import 'matching.dart'; import 'misc/error_screen.dart'; +import 'misc/stateful_navigation_shell.dart'; import 'pages/cupertino.dart'; import 'pages/custom_transition_page.dart'; import 'pages/material.dart'; @@ -127,7 +128,7 @@ class RouteBuilder { } } - void _buildRecursive( + GoRouterState? _buildRecursive( BuildContext context, RouteMatchList matchList, int startIndex, @@ -138,7 +139,7 @@ class RouteBuilder { GlobalKey navigatorKey, ) { if (startIndex >= matchList.matches.length) { - return; + return null; } final RouteMatch match = matchList.matches[startIndex]; @@ -163,8 +164,9 @@ class RouteBuilder { keyToPages.putIfAbsent(goRouteNavKey, () => >[]).add(page); - _buildRecursive(context, matchList, startIndex + 1, pop, routerNeglect, - keyToPages, newParams, navigatorKey); + return _buildRecursive(context, matchList, startIndex + 1, pop, + routerNeglect, keyToPages, newParams, navigatorKey) ?? + state; } else if (route is ShellRouteBase) { if (startIndex + 1 >= matchList.matches.length) { throw _RouteBuilderError('Shell routes must always have child routes'); @@ -185,19 +187,45 @@ class RouteBuilder { final RouteBase childRoute = matchList.matches[startIndex + 1].route; // The key to provide to the shell route's Navigator. - final GlobalKey shellNavigatorKey = + final GlobalKey? shellNavigatorKey = route.navigatorKeyForChildRoute(childRoute); + if (shellNavigatorKey == null) { + throw _RouteBuilderError( + 'Shell routes must always have a navigator key'); + } // Add an entry for the shell route's navigator keyToPages.putIfAbsent(shellNavigatorKey, () => >[]); - // Build the remaining pages - _buildRecursive(context, matchList, startIndex + 1, pop, routerNeglect, - keyToPages, newParams, shellNavigatorKey); + // Build the remaining pages and retrieve the state for the top of the + // navigation stack + final GoRouterState? topRouterState = _buildRecursive( + context, + matchList, + startIndex + 1, + pop, + routerNeglect, + keyToPages, + newParams, + shellNavigatorKey, + ); - final Widget child = _buildNavigator( + Widget child = _buildNavigator( pop, keyToPages[shellNavigatorKey]!, shellNavigatorKey); + if (route is StatefulShellRoute) { + if (topRouterState == null) { + throw _RouteBuilderError('StatefulShellRoute must always have a ' + 'StatefulRootRoute as a child'); + } + + child = _buildStatefulNavigationShell( + shellRoute: route, + navigator: child as Navigator, + shellRouterState: state, + topRouterState: topRouterState); + } + // Build the Page for this route final Page page = _buildPageForRoute(context, state, match, child: child); @@ -206,7 +234,10 @@ class RouteBuilder { keyToPages .putIfAbsent(parentNavigatorKey, () => >[]) .insert(shellPageIdx, page); + + return topRouterState; } + return null; } Navigator _buildNavigator( @@ -230,6 +261,21 @@ class RouteBuilder { ); } + StatefulNavigationShell _buildStatefulNavigationShell({ + required StatefulShellRoute shellRoute, + required Navigator navigator, + required GoRouterState shellRouterState, + required GoRouterState topRouterState, + }) { + return StatefulNavigationShell( + configuration: configuration, + shellRoute: shellRoute, + activeNavigator: navigator, + shellRouterState: shellRouterState, + topRouterState: topRouterState, + ); + } + /// Helper method that builds a [GoRouterState] object for the given [match] /// and [params]. @visibleForTesting @@ -270,7 +316,13 @@ class RouteBuilder { if (pageBuilder != null) { page = pageBuilder(context, state); } - } else if (route is ShellRouteBase) { + } else if (route is StatefulShellRoute) { + final ShellRoutePageBuilder? pageForShell = route.pageProvider; + assert(child != null, 'StatefulShellRoute must contain a child route'); + if (pageForShell != null) { + page = pageForShell(context, state, child!); + } + } else if (route is ShellRoute) { final ShellRoutePageBuilder? pageBuilder = route.pageBuilder; assert(child != null, 'ShellRoute must contain a child route'); if (pageBuilder != null) { @@ -312,13 +364,19 @@ class RouteBuilder { 'Attempt to build ShellRoute without a child widget'); } - final ShellRouteBuilder? builder = route.builder; + if (route is StatefulShellRoute) { + // StatefulShellRoute builder will already have been called at this + // point, to create childWidget + return childWidget; + } else if (route is ShellRoute) { + final ShellRouteBuilder? builder = route.builder; - if (builder == null) { - throw _RouteBuilderError('No builder provided to ShellRoute: $route'); - } + if (builder == null) { + throw _RouteBuilderError('No builder provided to ShellRoute: $route'); + } - return builder(context, state, childWidget); + return builder(context, state, childWidget); + } } throw _RouteBuilderException('Unsupported route type $route'); diff --git a/packages/go_router/lib/src/configuration.dart b/packages/go_router/lib/src/configuration.dart index fddcfebe1c51..02e50aae4e49 100644 --- a/packages/go_router/lib/src/configuration.dart +++ b/packages/go_router/lib/src/configuration.dart @@ -80,7 +80,7 @@ class RouteConfiguration { ...allowedKeys..add(route.navigatorKey) ], ); - } else if (route is PartitionedShellRoute) { + } else if (route is StatefulShellRoute) { checkParentNavigatorKeys( route.routes, >[ @@ -154,6 +154,31 @@ class RouteConfiguration { .toString(); } + /// Returns the full path to the specified route. + String fullPathForRoute(RouteBase route) { + return _fullPathForRoute(route, '', routes) ?? ''; + } + + static String? _fullPathForRoute( + RouteBase targetRoute, String parentFullpath, List routes) { + for (final RouteBase route in routes) { + final String fullPath = (route is GoRoute) + ? concatenatePaths(parentFullpath, route.path) + : parentFullpath; + + if (route == targetRoute) { + return fullPath; + } else { + final String? subRoutePath = + _fullPathForRoute(targetRoute, fullPath, route.routes); + if (subRoutePath != null) { + return subRoutePath; + } + } + } + return null; + } + @override String toString() { return 'RouterConfiguration: $routes'; diff --git a/packages/go_router/lib/src/delegate.dart b/packages/go_router/lib/src/delegate.dart index 498802cd0276..38efaaf130bc 100644 --- a/packages/go_router/lib/src/delegate.dart +++ b/packages/go_router/lib/src/delegate.dart @@ -70,10 +70,11 @@ class GoRouterDelegate extends RouterDelegate } else if (route is ShellRouteBase && childRoute != null) { // For shell routes, find the navigator key that should be used for the // child route in the current match list - final GlobalKey navigatorKey = + final GlobalKey? navigatorKey = route.navigatorKeyForChildRoute(childRoute); - final bool didPop = await navigatorKey.currentState!.maybePop(); + final bool didPop = + await navigatorKey?.currentState!.maybePop() ?? false; // Continue if didPop was false. if (didPop) { @@ -139,10 +140,10 @@ class GoRouterDelegate extends RouterDelegate } else if (route is ShellRouteBase && childRoute != null) { // For shell routes, find the navigator key that should be used for the // child route in the current match list - final GlobalKey navigatorKey = + final GlobalKey? navigatorKey = route.navigatorKeyForChildRoute(childRoute); - final bool canPop = navigatorKey.currentState!.canPop(); + final bool canPop = navigatorKey?.currentState!.canPop() ?? false; // Continue if canPop is false. if (canPop) { diff --git a/packages/go_router/lib/src/misc/stacked_navigation_shell.dart b/packages/go_router/lib/src/misc/stacked_navigation_shell.dart deleted file mode 100644 index 3232a5106608..000000000000 --- a/packages/go_router/lib/src/misc/stacked_navigation_shell.dart +++ /dev/null @@ -1,221 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'package:collection/collection.dart'; -import 'package:flutter/widgets.dart'; - -import '../router.dart'; -import '../state.dart'; - -/// Transition builder callback used by [StackedNavigationShell]. -/// -/// The builder is expected to return a transition powered by the provided -/// `animation` and wrapping the provided `child`. -/// -/// The `animation` provided to the builder always runs forward from 0.0 to 1.0. -typedef StackedNavigationTransitionBuilder = Widget Function( - BuildContext context, - Animation animation, - Widget child, -); - -/// Builder for the scaffold of a [StackedNavigationShell] -typedef StackedNavigationScaffoldBuilder = Widget Function( - BuildContext context, - int currentIndex, - List itemsState, - Widget scaffoldBody, -); - -/// Representation of a item in the stack of a [StackedNavigationShell] -class StackedNavigationItem { - /// Constructs an [StackedNavigationItem]. - StackedNavigationItem( - {required this.rootRoutePath, required this.navigatorKey}); - - /// The location/path of the root route of this navigation tree - final String rootRoutePath; - - /// Optional navigatorKey - final GlobalKey navigatorKey; -} - -/// Represents the current state of a [StackedNavigationItem] in a -/// [StackedNavigationShell] -class StackedNavigationItemState { - /// Constructs an [StackedNavigationItemState]. - StackedNavigationItemState(this.item); - - /// The [StackedNavigationItem] this state is representing. - final StackedNavigationItem item; - - /// The last location of this item. - String? lastLocation; - - /// The [Navigator] for this item. - Navigator? navigator; - - /// Gets the current location from the [currentRouterState] or falls back to - /// the root route location of the associated [item]. - String get currentLocation => lastLocation ?? item.rootRoutePath; -} - -/// Widget that maintains a stateful stack of [Navigator]s, using an -/// [IndexedStack]. -/// -/// Each item in the stack is represented by a [StackedNavigationItem], -/// specified in the `stackItems` parameter. The stack items will be used to -/// build the widgets containing the [Navigator] for each index in the stack. -/// Once a stack item (along with its Navigator) has been initialized, it will -/// remain in a widget tree, wrapped in an [Offstage] widget. -/// -/// The stacked navigation shell can be customized by specifying a -/// `scaffoldBuilder`, to build a widget that wraps the index stack. -class StackedNavigationShell extends StatefulWidget { - /// Constructs an [StackedNavigationShell]. - const StackedNavigationShell({ - required this.currentNavigator, - required this.currentRouterState, - required this.stackItems, - this.scaffoldBuilder, - this.transitionBuilder, - this.transitionDuration, - super.key, - }); - - /// The default transition duration - static const Duration defaultTransitionDuration = Duration(milliseconds: 400); - - /// The navigator for the currently active tab - final Navigator currentNavigator; - - /// The current router state - final GoRouterState currentRouterState; - - /// The tabs - final List stackItems; - - /// The scaffold builder - final StackedNavigationScaffoldBuilder? scaffoldBuilder; - - /// An optional transition builder for stack transitions - final StackedNavigationTransitionBuilder? transitionBuilder; - - /// The duration for stack transitions - final Duration? transitionDuration; - - @override - State createState() => _StackedNavigationShellState(); -} - -class _StackedNavigationShellState extends State - with SingleTickerProviderStateMixin { - int _currentIndex = 0; - late final AnimationController? _animationController; - late final List _items; - - int _findCurrentIndex() { - final int index = _items.indexWhere((StackedNavigationItemState i) => - i.item.navigatorKey == widget.currentNavigator.key); - return index < 0 ? 0 : index; - } - - @override - void initState() { - super.initState(); - _items = widget.stackItems - .map((StackedNavigationItem i) => StackedNavigationItemState(i)) - .toList(); - - if (widget.transitionBuilder != null) { - _animationController = AnimationController( - vsync: this, - duration: widget.transitionDuration ?? - StackedNavigationShell.defaultTransitionDuration); - _animationController?.forward(); - } else { - _animationController = null; - } - } - - @override - void didUpdateWidget(covariant StackedNavigationShell oldWidget) { - super.didUpdateWidget(oldWidget); - _updateForCurrentTab(); - } - - @override - void didChangeDependencies() { - super.didChangeDependencies(); - _updateForCurrentTab(); - } - - void _updateForCurrentTab() { - final int previousIndex = _currentIndex; - _currentIndex = _findCurrentIndex(); - - final StackedNavigationItemState itemState = _items[_currentIndex]; - itemState.navigator = widget.currentNavigator; - // Note: Would have been cleaner to be able to get the current location - // (full path) from GoRouterState, but currently that isn't possible, since - // the RouteMatchList doesn't seem to be updated properly on pop. - itemState.lastLocation = GoRouter.of(context).location; - - if (previousIndex != _currentIndex) { - _animationController?.forward(from: 0.0); - } - } - - @override - void dispose() { - _animationController?.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - final StackedNavigationScaffoldBuilder? scaffoldBuilder = - widget.scaffoldBuilder; - if (scaffoldBuilder != null) { - return scaffoldBuilder( - context, _currentIndex, _items, _buildIndexStack(context)); - } else { - return _buildIndexStack(context); - } - } - - Widget _buildIndexStack(BuildContext context) { - final List children = _items - .mapIndexed((int index, StackedNavigationItemState item) => - _buildNavigator(context, index, item)) - .toList(); - - final Widget indexedStack = - IndexedStack(index: _currentIndex, children: children); - - final StackedNavigationTransitionBuilder? transitionBuilder = - widget.transitionBuilder; - if (transitionBuilder != null) { - return transitionBuilder(context, _animationController!, indexedStack); - } else { - return indexedStack; - } - } - - Widget _buildNavigator(BuildContext context, int index, - StackedNavigationItemState navigationItem) { - final Navigator? navigator = navigationItem.navigator; - if (navigator == null) { - return const SizedBox.shrink(); - } - final bool isActive = index == _currentIndex; - return Offstage( - offstage: !isActive, - child: TickerMode( - enabled: isActive, - child: navigator, - ), - ); - } -} diff --git a/packages/go_router/lib/src/misc/stateful_navigation_shell.dart b/packages/go_router/lib/src/misc/stateful_navigation_shell.dart new file mode 100644 index 000000000000..6166f2e9e13d --- /dev/null +++ b/packages/go_router/lib/src/misc/stateful_navigation_shell.dart @@ -0,0 +1,214 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:collection/collection.dart'; +import 'package:flutter/widgets.dart'; + +import '../configuration.dart'; +import '../typedefs.dart'; + +/// Transition builder callback used by [StatefulNavigationShell]. +/// +/// The builder is expected to return a transition powered by the provided +/// `animation` and wrapping the provided `child`. +/// +/// The `animation` provided to the builder always runs forward from 0.0 to 1.0. +typedef StatefulNavigationTransitionBuilder = Widget Function( + BuildContext context, + Animation animation, + Widget child, +); + +/// [InheritedWidget] for providing a reference to the closest +/// [StatefulNavigationShellState]. +class InheritedStatefulNavigationShell extends InheritedWidget { + /// Constructs an [InheritedStatefulNavigationShell]. + const InheritedStatefulNavigationShell({ + required super.child, + required this.state, + super.key, + }); + + /// The [StatefulNavigationShellState] that is exposed by this InheritedWidget. + final StatefulNavigationShellState state; + + @override + bool updateShouldNotify( + covariant InheritedStatefulNavigationShell oldWidget) { + return state != oldWidget.state; + } +} + +/// Widget that maintains a stateful stack of [Navigator]s, using an +/// [IndexedStack]. +/// +/// Each item in the stack is represented by a [StackedNavigationItem], +/// specified in the `stackItems` parameter. The stack items will be used to +/// build the widgets containing the [Navigator] for each index in the stack. +/// Once a stack item (along with its Navigator) has been initialized, it will +/// remain in a widget tree, wrapped in an [Offstage] widget. +/// +/// The stacked navigation shell can be customized by specifying a +/// `scaffoldBuilder`, to build a widget that wraps the index stack. +class StatefulNavigationShell extends StatefulWidget { + /// Constructs an [StatefulNavigationShell]. + const StatefulNavigationShell({ + required this.configuration, + required this.shellRoute, + required this.activeNavigator, + required this.shellRouterState, + required this.topRouterState, + super.key, + }); + + /// The default transition duration + static const Duration defaultTransitionDuration = Duration(milliseconds: 400); + + /// The route configuration for the app. + final RouteConfiguration configuration; + + /// The associated [StatefulShellRoute] + final StatefulShellRoute shellRoute; + + /// The navigator for the currently active tab + final Navigator activeNavigator; + + /// The [GoRouterState] for navigation shell. + final GoRouterState shellRouterState; + + /// The [GoRouterState] for the top of the current navigation stack. + final GoRouterState topRouterState; + + @override + State createState() => StatefulNavigationShellState(); +} + +/// State for StatefulNavigationShell. +class StatefulNavigationShellState extends State + with SingleTickerProviderStateMixin { + int _currentIndex = 0; + + late final AnimationController? _animationController; + + late final List _childRouteState; + + StatefulNavigationTransitionBuilder? get _transitionBuilder => + widget.shellRoute.transitionBuilder; + + Duration? get _transitionDuration => widget.shellRoute.transitionDuration; + + int _findCurrentIndex() { + final int index = _childRouteState.indexWhere( + (ShellNavigationBranchState i) => + i.navigationItem.navigatorKey == widget.activeNavigator.key); + return index < 0 ? 0 : index; + } + + /// The current [StatefulShellRouteState] + StatefulShellRouteState get routeState => StatefulShellRouteState( + route: widget.shellRoute, + navigationBranchState: _childRouteState, + currentBranchIndex: _currentIndex); + + @override + void initState() { + super.initState(); + _childRouteState = widget.shellRoute.navigationBranches + .map((ShellNavigationBranchItem e) => ShellNavigationBranchState( + navigationItem: e, + rootRoutePath: widget.configuration.fullPathForRoute(e.rootRoute))) + .toList(); + + if (_transitionBuilder != null) { + _animationController = AnimationController( + vsync: this, + duration: _transitionDuration ?? + StatefulNavigationShell.defaultTransitionDuration); + _animationController?.forward(); + } else { + _animationController = null; + } + } + + @override + void didUpdateWidget(covariant StatefulNavigationShell oldWidget) { + super.didUpdateWidget(oldWidget); + _updateForCurrentTab(); + } + + @override + void didChangeDependencies() { + super.didChangeDependencies(); + _updateForCurrentTab(); + } + + void _updateForCurrentTab() { + final int previousIndex = _currentIndex; + _currentIndex = _findCurrentIndex(); + + final ShellNavigationBranchState itemState = + _childRouteState[_currentIndex]; + itemState.navigator = widget.activeNavigator; + itemState.topRouteState = widget.topRouterState; + + if (previousIndex != _currentIndex) { + _animationController?.forward(from: 0.0); + } + } + + @override + void dispose() { + _animationController?.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return InheritedStatefulNavigationShell( + state: this, + child: Builder(builder: (BuildContext context) { + final ShellRouteBuilder builder = widget.shellRoute.builder; + return builder( + context, + widget.shellRouterState, + _buildIndexStack(context), + ); + }), + ); + } + + Widget _buildIndexStack(BuildContext context) { + final List children = _childRouteState + .mapIndexed((int index, ShellNavigationBranchState item) => + _buildNavigator(context, index, item)) + .toList(); + + final Widget indexedStack = + IndexedStack(index: _currentIndex, children: children); + + final StatefulNavigationTransitionBuilder? transitionBuilder = + _transitionBuilder; + if (transitionBuilder != null) { + return transitionBuilder(context, _animationController!, indexedStack); + } else { + return indexedStack; + } + } + + Widget _buildNavigator(BuildContext context, int index, + ShellNavigationBranchState navigationItem) { + final Navigator? navigator = navigationItem.navigator; + if (navigator == null) { + return const SizedBox.shrink(); + } + final bool isActive = index == _currentIndex; + return Offstage( + offstage: !isActive, + child: TickerMode( + enabled: isActive, + child: navigator, + ), + ); + } +} diff --git a/packages/go_router/lib/src/route.dart b/packages/go_router/lib/src/route.dart index 6e749b109c23..fb540b51ac1c 100644 --- a/packages/go_router/lib/src/route.dart +++ b/packages/go_router/lib/src/route.dart @@ -6,8 +6,7 @@ import 'package:collection/collection.dart'; import 'package:flutter/widgets.dart'; import 'configuration.dart'; -import 'misc/errors.dart'; -import 'misc/stacked_navigation_shell.dart'; +import 'misc/stateful_navigation_shell.dart'; import 'pages/custom_transition_page.dart'; import 'path_utils.dart'; import 'typedefs.dart'; @@ -336,28 +335,13 @@ class GoRoute extends RouteBase { } /// Base class for classes that acts as a shell for child routes, such -/// as [ShellRoute] and [PartitionedShellRoute]. +/// as [ShellRoute] and [StatefulShellRoute]. abstract class ShellRouteBase extends RouteBase { - const ShellRouteBase._({this.builder, this.pageBuilder, super.routes}) - : super._(); - - /// The widget builder for a shell route. - /// - /// Similar to GoRoute builder, but with an additional child parameter. This - /// child parameter is the [Navigator] that will be used for the matching - /// sub-routes. - final ShellRouteBuilder? builder; - - /// The page builder for a shell route. - /// - /// Similar to GoRoute pageBuilder, but with an additional child parameter. - /// This child parameter is the [Navigator] that will be used for the matching - /// sub-routes. - final ShellRoutePageBuilder? pageBuilder; + const ShellRouteBase._({super.routes}) : super._(); /// Returns the key for the [Navigator] that is to be used for the specified /// child route. - GlobalKey navigatorKeyForChildRoute(RouteBase route); + GlobalKey? navigatorKeyForChildRoute(RouteBase route); } /// A route that displays a UI shell around the matching child route. @@ -458,8 +442,8 @@ abstract class ShellRouteBase extends RouteBase { class ShellRoute extends ShellRouteBase { /// Constructs a [ShellRoute]. ShellRoute({ - super.builder, - super.pageBuilder, + this.builder, + this.pageBuilder, super.routes, GlobalKey? navigatorKey, }) : assert(routes.isNotEmpty), @@ -473,13 +457,27 @@ class ShellRoute extends ShellRouteBase { } } + /// The widget builder for a shell route. + /// + /// Similar to GoRoute builder, but with an additional child parameter. This + /// child parameter is the [Navigator] that will be used for the matching + /// sub-routes. + final ShellRouteBuilder? builder; + + /// The page builder for a shell route. + /// + /// Similar to GoRoute pageBuilder, but with an additional child parameter. + /// This child parameter is the [Navigator] that will be used for the matching + /// sub-routes. + final ShellRoutePageBuilder? pageBuilder; + /// The [GlobalKey] to be used by the [Navigator] built for this route. /// All ShellRoutes build a Navigator by default. Child GoRoutes /// are placed onto this Navigator instead of the root Navigator. final GlobalKey navigatorKey; @override - GlobalKey navigatorKeyForChildRoute(RouteBase route) { + GlobalKey? navigatorKeyForChildRoute(RouteBase route) { return navigatorKey; } } @@ -488,14 +486,22 @@ class ShellRoute extends ShellRouteBase { /// routes. /// /// Similar to [ShellRoute], this route class places its sub routes on a -/// separate Navigator instead of the root Navigator. However, this route -/// class differs in that it uses a separate Navigator for each of its sub -/// route trees, making it possible to build a stateful nested navigation. This -/// is convenient when for instance implementing a UI with a +/// different Navigator than the root Navigator. However, this route class +/// differs in that it creates separate Navigators for each of its sub +/// route trees (branches), making it possible to build a stateful nested +/// navigation. This is convenient when for instance implementing a UI with a /// [BottomNavigationBar], with a persistent navigation state for each tab. /// +/// To access the current state of this route, to for instance access the +/// index of the current navigation branch - use the method +/// [StatefulShellRoute.of]. For example: +/// +/// ``` +/// final StatefulShellRouteState shellState = StatefulShellRoute.of(context); +/// ``` +/// /// Below is a simple example of how a router configuration with -/// PartitionedShellRoute could be achieved. In this example, a +/// StatefulShellRoute could be achieved. In this example, a /// BottomNavigationBar with two tabs is used, and each of the tabs gets its /// own Navigator. This Navigator will then be passed as the child argument /// of the [builder] function. @@ -509,19 +515,15 @@ class ShellRoute extends ShellRouteBase { /// final GoRouter _router = GoRouter( /// initialLocation: '/a', /// routes: [ -/// PartitionedShellRoute( -/// navigatorKeys: >[ -/// _tabANavigatorKey, _tabBNavigatorKey, -/// ], -/// builder: (BuildContext context, GoRouterState state, Widget child) { -/// return CustomScaffoldWithBottomNavigationBar( -/// currentNavigator: child as Navigator, -/// currentRouterState: state, -/// ); +/// StatefulShellRoute.branchRootRoutes( +/// builder: (BuildContext context, GoRouterState state, +/// Widget statefulShellNavigation) { +/// return ScaffoldWithNavBar(body: statefulShellNavigation); /// }, -/// routes: [ -/// /// The root of tab 'A' -/// GoRoute( +/// routes: [ +/// /// The first branch, i.e. root of tab 'A' +/// ShellBranchRoute( +/// navigatorKey: _tabANavigatorKey, /// path: '/a', /// builder: (BuildContext context, GoRouterState state) => /// const RootScreen(label: 'A', detailsPath: '/a/details'), @@ -534,9 +536,9 @@ class ShellRoute extends ShellRouteBase { /// ), /// ], /// ), -/// -/// /// The root of tab 'B' -/// GoRoute( +/// /// The second branch, i.e. root of tab 'B' +/// ShellBranchRoute( +/// navigatorKey: _tabBNavigatorKey, /// path: '/b', /// builder: (BuildContext context, GoRouterState state) => /// const RootScreen(label: 'B', detailsPath: '/b/details/1'), @@ -555,38 +557,33 @@ class ShellRoute extends ShellRouteBase { /// ); /// ``` /// -/// For scenarios where it's suitable to use an [IndexedStack] to manage the -/// navigators, consider using the [PartitionedShellRoute.stackedNavigationShell] -/// constructor instead to reduce boilerplate. For example: +/// When the [Page] for this route needs to be customized, you need to pass a +/// function for [pageProvider]. Note that this page provider doesn't replace +/// the [builder] function, but instead receives the stateful shell built by +/// [StatefulShellRoute] (using the builder function) as input. In other words, +/// you need to specify both when customizing a page. For example: /// /// ``` /// final GoRouter _router = GoRouter( /// initialLocation: '/a', /// routes: [ -/// PartitionedShellRoute.stackedNavigation( -/// stackItems: [ -/// StackedNavigationItem( -/// rootRoutePath: '/a', navigatorKey: _tabANavigatorKey), -/// StackedNavigationItem( -/// rootRoutePath: '/b', navigatorKey: _tabBNavigatorKey), -/// ], -/// scaffoldBuilder: (BuildContext context, int currentIndex, -/// List itemsState, -/// Widget scaffoldBody) { -/// return ScaffoldWithBottomNavigationBar( -/// currentIndex: currentIndex, -/// itemsState: itemsState, -/// body: scaffoldBody, -/// ); +/// StatefulShellRoute.branchRootRoutes( +/// builder: (BuildContext context, GoRouterState state, +/// Widget statefulShellNavigation) { +/// return ScaffoldWithNavBar(body: statefulShellNavigation); /// }, -/// routes: [ -/// /// The root of tab 'A' +/// pageProvider: +/// (BuildContext context, GoRouterState state, Widget statefulShell) { +/// return NoTransitionPage(child: statefulShell); +/// }, +/// routes: [ +/// /// The first branch, i.e. root of tab 'A' /// GoRoute( /// path: '/a', /// builder: (BuildContext context, GoRouterState state) => /// const RootScreen(label: 'A', detailsPath: '/a/details'), /// ), -/// /// The root of tab 'B' +/// /// The second branch, i.e. root of tab 'B' /// GoRoute( /// path: '/b', /// builder: (BuildContext context, GoRouterState state) => @@ -600,23 +597,28 @@ class ShellRoute extends ShellRouteBase { /// /// See [Stateful Nested Navigation](https://github.com/flutter/packages/blob/main/packages/go_router/example/lib/stateful_nested_navigation.dart) /// for a complete runnable example. -class PartitionedShellRoute extends ShellRouteBase { - /// Constructs a [PartitionedShellRoute] from a list of routes, each - /// representing the root of a navigation hierarchy. A separate - /// [Navigator] will be created for each of the root routes, using the - /// navigator key at the corresponding index in [navigatorKeys]. - PartitionedShellRoute({ - required List routes, - required this.navigatorKeys, - super.builder, - super.pageBuilder, - }) : assert(routes.isNotEmpty), - assert(navigatorKeys.length == routes.length), - assert(Set>.from(navigatorKeys).length == - routes.length), - assert(pageBuilder != null || builder != null, - 'builder or pageBuilder must be provided'), - super._(routes: routes) { +class StatefulShellRoute extends ShellRouteBase { + /// Constructs a [StatefulShellRoute] from a list of [ShellNavigationBranchItem] + /// items, each representing a root in a stateful navigation branch. + /// + /// A separate [Navigator] will be created for each of the branches, using + /// the navigator key specified in [ShellNavigationBranchItem]. + StatefulShellRoute({ + required this.navigationBranches, + required this.builder, + this.pageProvider, + this.transitionBuilder, + this.transitionDuration, + }) : assert(navigationBranches.isNotEmpty), + assert( + Set>.from(navigationBranches.map( + (ShellNavigationBranchItem e) => e.navigatorKey)).length == + navigationBranches.length, + 'Navigator keys must be unique'), + super._( + routes: navigationBranches + .map((ShellNavigationBranchItem e) => e.rootRoute) + .toList()) { for (int i = 0; i < routes.length; ++i) { final RouteBase route = routes[i]; if (route is GoRoute) { @@ -626,48 +628,106 @@ class PartitionedShellRoute extends ShellRouteBase { } } - /// Constructs a [PartitionedShellRoute] that places its navigators in an - /// [IndexedStack], managed by a [StackedNavigationShell]. + /// Constructs a [StatefulShellRoute] from a list of [GoRoute]s, each + /// representing a root in a stateful navigation branch. /// - /// Each route in the `routes` parameter must correspond to a - /// [StackedNavigationItem], specified in the `stackItems` parameter. - /// The stacked navigation shell can be implemented by specifying a - /// `scaffoldBuilder`, to build a widget that wraps the indexed stack. - factory PartitionedShellRoute.stackedNavigationShell({ - required List routes, - required List stackItems, - StackedNavigationScaffoldBuilder? scaffoldBuilder, - StackedNavigationTransitionBuilder? transitionBuilder, + /// This constructor provides a shorthand form of creating a + /// StatefulShellRoute from a list of GoRoutes instead of + /// [ShellNavigationBranchItem]s. Each GoRoute provides the navigator key + /// (via [GoRoute.parentNavigatorKey]) that will be used to create the + /// separate [Navigator]s for the routes. + StatefulShellRoute.navigationBranchRoutes({ + required List routes, + required ShellRouteBuilder builder, + ShellRoutePageBuilder? pageProvider, + StatefulNavigationTransitionBuilder? transitionBuilder, Duration? transitionDuration, - }) { - return PartitionedShellRoute( - routes: routes, - navigatorKeys: stackItems - .map((StackedNavigationItem e) => e.navigatorKey) - .toList(), - builder: (BuildContext context, GoRouterState state, Widget child) { - assert(child is Navigator); - return StackedNavigationShell( - currentNavigator: child as Navigator, - currentRouterState: state, - stackItems: stackItems, - scaffoldBuilder: scaffoldBuilder, - transitionBuilder: transitionBuilder, - transitionDuration: transitionDuration, - ); - }); - } + }) : this( + navigationBranches: routes.map((GoRoute e) { + assert( + e.parentNavigatorKey != null, + 'Each route must specify a ' + 'parentNavigatorKey'); + return ShellNavigationBranchItem( + rootRoute: e, + navigatorKey: e.parentNavigatorKey!, + ); + }).toList(), + builder: builder, + pageProvider: pageProvider, + transitionBuilder: transitionBuilder, + transitionDuration: transitionDuration, + ); + + /// Representations of the different stateful navigation branches that this + /// shell route will manage. Each branch identifies the Navigator to be used + /// (via the navigatorKey) and the route that will be used as the root of the + /// navigation branch. + final List navigationBranches; + + /// The widget builder for a stateful shell route. + /// + /// Similar to GoRoute builder, but with an additional child parameter. This + /// child parameter is the Widget responsible for managing - and switching + /// between - the navigators for the different branches of this + /// StatefulShellRoute. The Widget returned by this function will be embedded + /// into the stateful shell, making it possible to access the + /// [StatefulShellRouteState] via the method [StatefulShellRoute.of]. + final ShellRouteBuilder builder; + + /// Function for customizing the [Page] for this stateful shell. + /// + /// Similar to GoRoute pageBuilder, but with an additional child parameter. + /// Unlike GoRoute however, this function is used in combination with + /// [builder], and the child parameter will be the stateful shell already + /// built for this route, using the builder function. + final ShellRoutePageBuilder? pageProvider; + + /// An optional transition builder for transitions when switching navigation + /// branch in the shell. + final StatefulNavigationTransitionBuilder? transitionBuilder; + + /// The duration for shell transitions + final Duration? transitionDuration; /// The navigator keys of the navigators created by this route. - final List> navigatorKeys; + List> get navigatorKeys => navigationBranches + .map((ShellNavigationBranchItem e) => e.navigatorKey) + .toList(); @override - GlobalKey navigatorKeyForChildRoute(RouteBase route) { + GlobalKey? navigatorKeyForChildRoute(RouteBase route) { final int routeIndex = routes.indexOf(route); if (routeIndex < 0) { - throw GoError('Route $route is not a child of this ' - 'PartitionedShellRoute $this'); + return null; } return navigatorKeys[routeIndex]; } + + /// Gets the state for the nearest stateful shell route in the Widget tree. + static StatefulShellRouteState of(BuildContext context) { + final InheritedStatefulNavigationShell? inherited = context + .dependOnInheritedWidgetOfExactType(); + assert(inherited != null, + 'No InheritedStatefulNavigationShell found in context'); + return inherited!.state.routeState; + } +} + +/// Representation of a separate branch in a stateful navigation tree, used to +/// configure [StatefulShellRoute]. +class ShellNavigationBranchItem { + /// Constructs a [ShellNavigationBranchItem]. + ShellNavigationBranchItem({ + required this.rootRoute, + required this.navigatorKey, + }); + + /// The root route of the navigation branch. + final RouteBase rootRoute; + + /// The [GlobalKey] to be used by the [Navigator] built for the navigation + /// branch represented by this object. All child routes will also be placed + /// onto this Navigator instead of the root Navigator. + final GlobalKey navigatorKey; } diff --git a/packages/go_router/lib/src/state.dart b/packages/go_router/lib/src/state.dart index 7529c22a036c..88a62d9f76fe 100644 --- a/packages/go_router/lib/src/state.dart +++ b/packages/go_router/lib/src/state.dart @@ -2,7 +2,7 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'package:flutter/foundation.dart'; +import 'package:flutter/widgets.dart'; import 'configuration.dart'; @@ -82,3 +82,58 @@ class GoRouterState { params: params, queryParams: queryParams); } } + +/// The current state for a [StatefulShellRoute]. +class StatefulShellRouteState { + /// Constructs a [StatefulShellRouteState]. + StatefulShellRouteState( + {required this.route, + required this.navigationBranchState, + required this.currentBranchIndex}); + + /// The associated [StatefulShellRoute] + final StatefulShellRoute route; + + /// The state for all separate navigation branches associated with a + /// [StatefulShellRoute]. + final List navigationBranchState; + + /// The index of the currently active navigation branch. + final int currentBranchIndex; + + /// Gets the current location from the [topRouteState] or falls back to + /// the root path of the associated [route]. + String get currentLocation => + navigationBranchState[currentBranchIndex].currentLocation; +} + +/// The current state for a particular navigation branch +/// ([ShellNavigationBranchItem]) of a [StatefulShellRoute]. +class ShellNavigationBranchState { + /// Constructs a [ShellNavigationBranchState]. + ShellNavigationBranchState({ + required this.navigationItem, + required this.rootRoutePath, + }); + + /// The associated [ShellNavigationBranchItem] + final ShellNavigationBranchItem navigationItem; + + /// The full path at which root route for the navigation branch is reachable. + final String rootRoutePath; + + /// The [Navigator] for this navigation branch in a [StatefulShellRoute]. This + /// field will typically not be set until this route tree has been navigated + /// to at least once. + Navigator? navigator; + + /// The [GoRouterState] for the top of the current navigation stack. + GoRouterState? topRouteState; + + /// Gets the current location from the [topRouteState] or falls back to + /// the root path of the associated [route]. + String get currentLocation => topRouteState?.location ?? rootRoutePath; + + /// The root route for the navigation branch. + RouteBase get route => navigationItem.rootRoute; +} diff --git a/packages/go_router/test/builder_test.dart b/packages/go_router/test/builder_test.dart index aeecfbb8f149..39eabed6fb57 100644 --- a/packages/go_router/test/builder_test.dart +++ b/packages/go_router/test/builder_test.dart @@ -108,18 +108,15 @@ void main() { expect(find.byType(_DetailsScreen), findsOneWidget); }); - testWidgets('Builds PartitionedShellRoute', (WidgetTester tester) async { + testWidgets('Builds StatefulShellRoute', (WidgetTester tester) async { final GlobalKey key = GlobalKey(debugLabel: 'key'); final RouteConfiguration config = RouteConfiguration( routes: [ - PartitionedShellRoute( + StatefulShellRoute.navigationBranchRoutes( builder: (BuildContext context, GoRouterState state, Widget child) => child, - navigatorKeys: >[ - key, - ], routes: [ GoRoute( parentNavigatorKey: key, diff --git a/packages/go_router/test/configuration_test.dart b/packages/go_router/test/configuration_test.dart index 2629c24f0638..cab0e28f8ea3 100644 --- a/packages/go_router/test/configuration_test.dart +++ b/packages/go_router/test/configuration_test.dart @@ -81,14 +81,15 @@ void main() { }); test( - 'throws when PartitionedShellRoute is missing a navigator key for a ' + 'throws when StatefulShellRoute is missing a navigator key for a ' 'child route', () { final GlobalKey root = GlobalKey(debugLabel: 'root'); final GlobalKey keyA = GlobalKey(debugLabel: 'A'); final List shellRouteChildren = [ - GoRoute(path: '/a', builder: _mockScreenBuilder), + GoRoute( + path: '/a', builder: _mockScreenBuilder, parentNavigatorKey: keyA), GoRoute(path: '/b', builder: _mockScreenBuilder), ]; expect( @@ -96,10 +97,8 @@ void main() { RouteConfiguration( navigatorKey: root, routes: [ - PartitionedShellRoute( - routes: shellRouteChildren, - navigatorKeys: >[keyA], - builder: _mockShellBuilder), + StatefulShellRoute.navigationBranchRoutes( + routes: shellRouteChildren, builder: _mockShellBuilder), ], redirectLimit: 10, topRedirect: (BuildContext context, GoRouterState state) { @@ -111,24 +110,24 @@ void main() { ); }); - test('throws when PartitionedShellRoute has duplicate navigator keys', () { + test('throws when StatefulShellRoute has duplicate navigator keys', () { final GlobalKey root = GlobalKey(debugLabel: 'root'); final GlobalKey keyA = GlobalKey(debugLabel: 'A'); final List shellRouteChildren = [ - GoRoute(path: '/a', builder: _mockScreenBuilder), - GoRoute(path: '/b', builder: _mockScreenBuilder), + GoRoute( + path: '/a', builder: _mockScreenBuilder, parentNavigatorKey: keyA), + GoRoute( + path: '/b', builder: _mockScreenBuilder, parentNavigatorKey: keyA), ]; expect( () { RouteConfiguration( navigatorKey: root, routes: [ - PartitionedShellRoute( - routes: shellRouteChildren, - navigatorKeys: >[keyA, keyA], - builder: _mockShellBuilder), + StatefulShellRoute.navigationBranchRoutes( + routes: shellRouteChildren, builder: _mockShellBuilder), ], redirectLimit: 10, topRedirect: (BuildContext context, GoRouterState state) { @@ -141,7 +140,7 @@ void main() { }); test( - 'throws when a child of PartitionedShellRoute has an incorrect ' + 'throws when a child of StatefulShellRoute has an incorrect ' 'parentNavigatorKey', () { final GlobalKey root = GlobalKey(debugLabel: 'root'); @@ -149,26 +148,25 @@ void main() { GlobalKey(); final GlobalKey sectionBNavigatorKey = GlobalKey(); - final List shellRouteChildren = [ - GoRoute( - path: '/a', - builder: _mockScreenBuilder, - parentNavigatorKey: sectionBNavigatorKey), - GoRoute( - path: '/b', - builder: _mockScreenBuilder, - parentNavigatorKey: sectionANavigatorKey), - ]; + final GoRoute routeA = GoRoute( + path: '/a', + builder: _mockScreenBuilder, + parentNavigatorKey: sectionBNavigatorKey); + final GoRoute routeB = GoRoute( + path: '/b', + builder: _mockScreenBuilder, + parentNavigatorKey: sectionANavigatorKey); expect( () { RouteConfiguration( navigatorKey: root, routes: [ - PartitionedShellRoute( - routes: shellRouteChildren, - navigatorKeys: >[ - sectionANavigatorKey, - sectionBNavigatorKey + StatefulShellRoute( + navigationBranches: [ + ShellNavigationBranchItem( + rootRoute: routeA, navigatorKey: sectionANavigatorKey), + ShellNavigationBranchItem( + rootRoute: routeB, navigatorKey: sectionBNavigatorKey), ], builder: _mockShellBuilder), ], @@ -556,6 +554,93 @@ void main() { throwsAssertionError, ); }); + + test( + 'Reports correct full path for route', + () { + final GoRoute routeC1 = GoRoute( + path: 'c1', + builder: _mockScreenBuilder, + ); + final GoRoute routeY2 = + GoRoute(path: 'y2', builder: _mockScreenBuilder, routes: [ + GoRoute( + path: 'z2', + builder: _mockScreenBuilder, + ), + ]); + final GoRoute routeZ1 = GoRoute( + path: 'z1/:param', + builder: _mockScreenBuilder, + ); + final RouteConfiguration config = RouteConfiguration( + navigatorKey: GlobalKey(debugLabel: 'root'), + routes: [ + ShellRoute( + routes: [ + GoRoute( + path: '/', + builder: _mockScreenBuilder, + routes: [ + GoRoute( + path: 'a', + builder: _mockScreenBuilder, + routes: [ + ShellRoute( + routes: [ + GoRoute( + path: 'b1', + builder: _mockScreenBuilder, + routes: [ + routeC1, + ], + ), + GoRoute( + path: 'b2', + builder: _mockScreenBuilder, + routes: [ + GoRoute( + path: 'c2', + builder: _mockScreenBuilder, + ), + ], + ), + ], + ), + ], + ), + GoRoute( + path: 'x', + builder: _mockScreenBuilder, + routes: [ + ShellRoute( + routes: [ + GoRoute( + path: 'y1', + builder: _mockScreenBuilder, + routes: [routeZ1], + ), + routeY2, + ], + ), + ], + ), + ], + ), + ], + ), + ], + redirectLimit: 10, + topRedirect: (BuildContext context, GoRouterState state) { + return null; + }, + ); + + expect('/a/b1/c1', config.fullPathForRoute(routeC1)); + expect('/x/y2', config.fullPathForRoute(routeY2)); + expect('/x/y1/z1/:param', config.fullPathForRoute(routeZ1)); + }, + ); } class _MockScreen extends StatelessWidget { diff --git a/packages/go_router/test/go_router_test.dart b/packages/go_router/test/go_router_test.dart index a141d24519c8..d74824a03941 100644 --- a/packages/go_router/test/go_router_test.dart +++ b/packages/go_router/test/go_router_test.dart @@ -2405,7 +2405,7 @@ void main() { }); testWidgets( - 'Navigates to correct nested navigation tree in PartitionedShellRoute ' + 'Navigates to correct nested navigation tree in StatefulShellRoute ' 'and maintains state', (WidgetTester tester) async { final GlobalKey rootNavigatorKey = GlobalKey(); @@ -2416,16 +2416,10 @@ void main() { final GlobalKey statefulWidgetKey = GlobalKey(); - final List stackItems = [ - StackedNavigationItem( - rootRoutePath: '/a', navigatorKey: sectionANavigatorKey), - StackedNavigationItem( - rootRoutePath: '/b', navigatorKey: sectionBNavigatorKey), - ]; - final List routes = [ - PartitionedShellRoute.stackedNavigationShell( - stackItems: stackItems, + StatefulShellRoute.navigationBranchRoutes( + builder: (BuildContext context, GoRouterState state, Widget child) => + child, routes: [ GoRoute( parentNavigatorKey: sectionANavigatorKey, @@ -2480,7 +2474,7 @@ void main() { }); testWidgets( - 'Pops from the correct Navigator in a PartitionedShellRoute when the ' + 'Pops from the correct Navigator in a StatefulShellRoute when the ' 'Android back button is pressed', (WidgetTester tester) async { final GlobalKey rootNavigatorKey = GlobalKey(); @@ -2489,16 +2483,10 @@ void main() { final GlobalKey sectionBNavigatorKey = GlobalKey(); - final List stackItems = [ - StackedNavigationItem( - rootRoutePath: '/a', navigatorKey: sectionANavigatorKey), - StackedNavigationItem( - rootRoutePath: '/b', navigatorKey: sectionBNavigatorKey), - ]; - final List routes = [ - PartitionedShellRoute.stackedNavigationShell( - stackItems: stackItems, + StatefulShellRoute.navigationBranchRoutes( + builder: (BuildContext context, GoRouterState state, Widget child) => + child, routes: [ GoRoute( parentNavigatorKey: sectionANavigatorKey, @@ -2722,7 +2710,7 @@ void main() { ); testWidgets( - 'It checks if PartitionedShellRoute navigators can pop', + 'It checks if StatefulShellRoute navigators can pop', (WidgetTester tester) async { final GlobalKey rootNavigatorKey = GlobalKey(); @@ -2734,11 +2722,7 @@ void main() { navigatorKey: rootNavigatorKey, initialLocation: '/a', routes: [ - PartitionedShellRoute( - navigatorKeys: >[ - shellNavigatorKeyA, - shellNavigatorKeyB - ], + StatefulShellRoute.navigationBranchRoutes( builder: (BuildContext context, GoRouterState state, Widget child) { return Scaffold( @@ -2749,6 +2733,7 @@ void main() { routes: [ GoRoute( path: '/a', + parentNavigatorKey: shellNavigatorKeyA, builder: (BuildContext context, _) { return const Scaffold( body: Text('Screen A'), @@ -2757,6 +2742,7 @@ void main() { ), GoRoute( path: '/b', + parentNavigatorKey: shellNavigatorKeyB, builder: (BuildContext context, _) { return const Scaffold( body: Text('Screen B'), @@ -2793,7 +2779,7 @@ void main() { expect(find.text('Screen B detail', skipOffstage: false), findsOneWidget); expect(router.canPop(), true); - // Verify that it is actually the PartitionedShellRoute that reports + // Verify that it is actually the StatefulShellRoute that reports // canPop = true expect(rootNavigatorKey.currentState?.canPop(), false); }, From 59c19e7052326d2f7f26ec1fd05ba52bf1e57b13 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20Lo=CC=88fstrand?= Date: Mon, 10 Oct 2022 14:16:09 +0200 Subject: [PATCH 027/112] Some refactoring (mostly naming and code readability). --- .../lib/stateful_nested_navigation.dart | 29 +++--- packages/go_router/lib/go_router.dart | 4 +- packages/go_router/lib/src/builder.dart | 13 +-- .../src/misc/stateful_navigation_shell.dart | 25 +++-- packages/go_router/lib/src/route.dart | 92 ++++++++++--------- packages/go_router/lib/src/state.dart | 43 +++++---- packages/go_router/test/builder_test.dart | 2 +- .../go_router/test/configuration_test.dart | 18 ++-- packages/go_router/test/go_router_test.dart | 6 +- 9 files changed, 121 insertions(+), 111 deletions(-) diff --git a/packages/go_router/example/lib/stateful_nested_navigation.dart b/packages/go_router/example/lib/stateful_nested_navigation.dart index 42c82fb2daa5..bb95bc30b470 100644 --- a/packages/go_router/example/lib/stateful_nested_navigation.dart +++ b/packages/go_router/example/lib/stateful_nested_navigation.dart @@ -36,20 +36,14 @@ class NestedTabNavigationExampleApp extends StatelessWidget { routes: [ /// Custom top shell route - wraps the below routes in a scaffold with /// a bottom tab navigator (ScaffoldWithNavBar). Each tab will use its own - /// Navigator, provided by StatefulShellRoute. - StatefulShellRoute.navigationBranchRoutes( + /// Navigator, as specified by the navigatorKey for each root route + /// (branch). For more customization options for the route branches, see + /// the default constructor for StatefulShellRoute. + StatefulShellRoute.rootRoutes( builder: (BuildContext context, GoRouterState state, Widget statefulShellNavigation) { return ScaffoldWithNavBar(body: statefulShellNavigation); }, - pageProvider: - (BuildContext context, GoRouterState state, Widget statefulShell) { - return NoTransitionPage(child: statefulShell); - }, - // A transition builder is optional: - // transitionBuilder: - // (BuildContext context, Animation animation, Widget child) => - // FadeTransition(opacity: animation, child: child), routes: [ /// The screen to display as the root in the first tab of the bottom /// navigation bar. @@ -91,6 +85,18 @@ class NestedTabNavigationExampleApp extends StatelessWidget { ], ), ], + + /// If you need to customize the Page for StatefulShellRoute, pass a + /// pageProvider function in addition to the builder, for example: + // pageProvider: + // (BuildContext context, GoRouterState state, Widget statefulShell) { + // return NoTransitionPage(child: statefulShell); + // }, + /// To customize shell route branch transitions, provide a transition + /// builder, for example: + // transitionBuilder: + // (BuildContext context, Animation animation, Widget child) => + // FadeTransition(opacity: animation, child: child), ), ], ); @@ -139,8 +145,7 @@ class ScaffoldWithNavBar extends StatelessWidget { ); } - void _onItemTapped( - BuildContext context, ShellNavigationBranchState routeState) { + void _onItemTapped(BuildContext context, ShellRouteBranchState routeState) { GoRouter.of(context).go(routeState.currentLocation); } } diff --git a/packages/go_router/lib/go_router.dart b/packages/go_router/lib/go_router.dart index 2a9205f99d43..44c0fe9caeb5 100644 --- a/packages/go_router/lib/go_router.dart +++ b/packages/go_router/lib/go_router.dart @@ -11,8 +11,8 @@ export 'src/configuration.dart' GoRoute, GoRouterState, RouteBase, - ShellNavigationBranchItem, - ShellNavigationBranchState, + ShellRouteBranch, + ShellRouteBranchState, ShellRoute, StatefulShellRoute, StatefulShellRouteState; diff --git a/packages/go_router/lib/src/builder.dart b/packages/go_router/lib/src/builder.dart index 703fe1f233fc..bfeb7257211a 100644 --- a/packages/go_router/lib/src/builder.dart +++ b/packages/go_router/lib/src/builder.dart @@ -215,15 +215,16 @@ class RouteBuilder { if (route is StatefulShellRoute) { if (topRouterState == null) { - throw _RouteBuilderError('StatefulShellRoute must always have a ' - 'StatefulRootRoute as a child'); + throw _RouteBuilderError('StatefulShellRoute cannot be at the top of ' + 'the navigation stack'); } child = _buildStatefulNavigationShell( - shellRoute: route, - navigator: child as Navigator, - shellRouterState: state, - topRouterState: topRouterState); + shellRoute: route, + navigator: child as Navigator, + shellRouterState: state, + topRouterState: topRouterState, + ); } // Build the Page for this route diff --git a/packages/go_router/lib/src/misc/stateful_navigation_shell.dart b/packages/go_router/lib/src/misc/stateful_navigation_shell.dart index 6166f2e9e13d..e22addd8bdf1 100644 --- a/packages/go_router/lib/src/misc/stateful_navigation_shell.dart +++ b/packages/go_router/lib/src/misc/stateful_navigation_shell.dart @@ -91,7 +91,7 @@ class StatefulNavigationShellState extends State late final AnimationController? _animationController; - late final List _childRouteState; + late final List _childRouteState; StatefulNavigationTransitionBuilder? get _transitionBuilder => widget.shellRoute.transitionBuilder; @@ -99,9 +99,8 @@ class StatefulNavigationShellState extends State Duration? get _transitionDuration => widget.shellRoute.transitionDuration; int _findCurrentIndex() { - final int index = _childRouteState.indexWhere( - (ShellNavigationBranchState i) => - i.navigationItem.navigatorKey == widget.activeNavigator.key); + final int index = _childRouteState.indexWhere((ShellRouteBranchState i) => + i.navigationItem.navigatorKey == widget.activeNavigator.key); return index < 0 ? 0 : index; } @@ -114,10 +113,11 @@ class StatefulNavigationShellState extends State @override void initState() { super.initState(); - _childRouteState = widget.shellRoute.navigationBranches - .map((ShellNavigationBranchItem e) => ShellNavigationBranchState( - navigationItem: e, - rootRoutePath: widget.configuration.fullPathForRoute(e.rootRoute))) + _childRouteState = widget.shellRoute.branches + .map((ShellRouteBranch e) => ShellRouteBranchState( + navigationItem: e, + rootRoutePath: widget.configuration.fullPathForRoute(e.rootRoute), + )) .toList(); if (_transitionBuilder != null) { @@ -147,8 +147,7 @@ class StatefulNavigationShellState extends State final int previousIndex = _currentIndex; _currentIndex = _findCurrentIndex(); - final ShellNavigationBranchState itemState = - _childRouteState[_currentIndex]; + final ShellRouteBranchState itemState = _childRouteState[_currentIndex]; itemState.navigator = widget.activeNavigator; itemState.topRouteState = widget.topRouterState; @@ -180,7 +179,7 @@ class StatefulNavigationShellState extends State Widget _buildIndexStack(BuildContext context) { final List children = _childRouteState - .mapIndexed((int index, ShellNavigationBranchState item) => + .mapIndexed((int index, ShellRouteBranchState item) => _buildNavigator(context, index, item)) .toList(); @@ -196,8 +195,8 @@ class StatefulNavigationShellState extends State } } - Widget _buildNavigator(BuildContext context, int index, - ShellNavigationBranchState navigationItem) { + Widget _buildNavigator( + BuildContext context, int index, ShellRouteBranchState navigationItem) { final Navigator? navigator = navigationItem.navigator; if (navigator == null) { return const SizedBox.shrink(); diff --git a/packages/go_router/lib/src/route.dart b/packages/go_router/lib/src/route.dart index fb540b51ac1c..518340e1f379 100644 --- a/packages/go_router/lib/src/route.dart +++ b/packages/go_router/lib/src/route.dart @@ -487,13 +487,14 @@ class ShellRoute extends ShellRouteBase { /// /// Similar to [ShellRoute], this route class places its sub routes on a /// different Navigator than the root Navigator. However, this route class -/// differs in that it creates separate Navigators for each of its sub -/// route trees (branches), making it possible to build a stateful nested -/// navigation. This is convenient when for instance implementing a UI with a -/// [BottomNavigationBar], with a persistent navigation state for each tab. +/// differs in that it creates separate Navigators for each of its nested +/// route branches (route trees), making it possible to build a stateful +/// nested navigation. This is convenient when for instance implementing a UI +/// with a [BottomNavigationBar], with a persistent navigation state for each +/// tab. /// /// To access the current state of this route, to for instance access the -/// index of the current navigation branch - use the method +/// index of the current route branch - use the method /// [StatefulShellRoute.of]. For example: /// /// ``` @@ -598,27 +599,21 @@ class ShellRoute extends ShellRouteBase { /// See [Stateful Nested Navigation](https://github.com/flutter/packages/blob/main/packages/go_router/example/lib/stateful_nested_navigation.dart) /// for a complete runnable example. class StatefulShellRoute extends ShellRouteBase { - /// Constructs a [StatefulShellRoute] from a list of [ShellNavigationBranchItem] - /// items, each representing a root in a stateful navigation branch. + /// Constructs a [StatefulShellRoute] from a list of [ShellRouteBranch], each + /// representing a root in a stateful route branch. /// /// A separate [Navigator] will be created for each of the branches, using - /// the navigator key specified in [ShellNavigationBranchItem]. + /// the navigator key specified in [ShellRouteBranch]. StatefulShellRoute({ - required this.navigationBranches, + required this.branches, required this.builder, this.pageProvider, this.transitionBuilder, this.transitionDuration, - }) : assert(navigationBranches.isNotEmpty), - assert( - Set>.from(navigationBranches.map( - (ShellNavigationBranchItem e) => e.navigatorKey)).length == - navigationBranches.length, + }) : assert(branches.isNotEmpty), + assert(_uniqueNavigatorKeys(branches).length == branches.length, 'Navigator keys must be unique'), - super._( - routes: navigationBranches - .map((ShellNavigationBranchItem e) => e.rootRoute) - .toList()) { + super._(routes: _rootRoutes(branches)) { for (int i = 0; i < routes.length; ++i) { final RouteBase route = routes[i]; if (route is GoRoute) { @@ -629,29 +624,24 @@ class StatefulShellRoute extends ShellRouteBase { } /// Constructs a [StatefulShellRoute] from a list of [GoRoute]s, each - /// representing a root in a stateful navigation branch. + /// representing a root in a stateful route branch. /// /// This constructor provides a shorthand form of creating a /// StatefulShellRoute from a list of GoRoutes instead of - /// [ShellNavigationBranchItem]s. Each GoRoute provides the navigator key + /// [ShellRouteBranch]s. Each GoRoute provides the navigator key /// (via [GoRoute.parentNavigatorKey]) that will be used to create the /// separate [Navigator]s for the routes. - StatefulShellRoute.navigationBranchRoutes({ + StatefulShellRoute.rootRoutes({ required List routes, required ShellRouteBuilder builder, ShellRoutePageBuilder? pageProvider, StatefulNavigationTransitionBuilder? transitionBuilder, Duration? transitionDuration, }) : this( - navigationBranches: routes.map((GoRoute e) { - assert( - e.parentNavigatorKey != null, - 'Each route must specify a ' - 'parentNavigatorKey'); - return ShellNavigationBranchItem( - rootRoute: e, - navigatorKey: e.parentNavigatorKey!, - ); + branches: routes.map((GoRoute e) { + final GlobalKey? key = e.parentNavigatorKey; + assert(key != null, 'Each route must specify a parentNavigatorKey'); + return ShellRouteBranch(rootRoute: e, navigatorKey: key!); }).toList(), builder: builder, pageProvider: pageProvider, @@ -659,11 +649,11 @@ class StatefulShellRoute extends ShellRouteBase { transitionDuration: transitionDuration, ); - /// Representations of the different stateful navigation branches that this - /// shell route will manage. Each branch identifies the Navigator to be used + /// Representations of the different stateful route branches that this + /// shell route will manage. Each branch identifies the [Navigator] to be used /// (via the navigatorKey) and the route that will be used as the root of the - /// navigation branch. - final List navigationBranches; + /// route branch. + final List branches; /// The widget builder for a stateful shell route. /// @@ -691,9 +681,8 @@ class StatefulShellRoute extends ShellRouteBase { final Duration? transitionDuration; /// The navigator keys of the navigators created by this route. - List> get navigatorKeys => navigationBranches - .map((ShellNavigationBranchItem e) => e.navigatorKey) - .toList(); + List> get navigatorKeys => + branches.map((ShellRouteBranch e) => e.navigatorKey).toList(); @override GlobalKey? navigatorKeyForChildRoute(RouteBase route) { @@ -701,7 +690,7 @@ class StatefulShellRoute extends ShellRouteBase { if (routeIndex < 0) { return null; } - return navigatorKeys[routeIndex]; + return branches[routeIndex].navigatorKey; } /// Gets the state for the nearest stateful shell route in the Widget tree. @@ -712,22 +701,35 @@ class StatefulShellRoute extends ShellRouteBase { 'No InheritedStatefulNavigationShell found in context'); return inherited!.state.routeState; } + + static Set> _uniqueNavigatorKeys( + List branches) => + Set>.from( + branches.map((ShellRouteBranch e) => e.navigatorKey)); + + static List _rootRoutes(List branches) => + branches.map((ShellRouteBranch e) => e.rootRoute).toList(); } /// Representation of a separate branch in a stateful navigation tree, used to /// configure [StatefulShellRoute]. -class ShellNavigationBranchItem { - /// Constructs a [ShellNavigationBranchItem]. - ShellNavigationBranchItem({ - required this.rootRoute, +class ShellRouteBranch { + /// Constructs a [ShellRouteBranch]. + ShellRouteBranch({ required this.navigatorKey, + required this.rootRoute, + this.defaultLocation, }); - /// The root route of the navigation branch. - final RouteBase rootRoute; - /// The [GlobalKey] to be used by the [Navigator] built for the navigation /// branch represented by this object. All child routes will also be placed /// onto this Navigator instead of the root Navigator. final GlobalKey navigatorKey; + + /// The root route of the route branch. + final RouteBase rootRoute; + + /// The default location for this route branch. If none is specified, the + /// location of the [rootRoute] will be used. + final String? defaultLocation; } diff --git a/packages/go_router/lib/src/state.dart b/packages/go_router/lib/src/state.dart index 88a62d9f76fe..e369eceb8373 100644 --- a/packages/go_router/lib/src/state.dart +++ b/packages/go_router/lib/src/state.dart @@ -86,19 +86,20 @@ class GoRouterState { /// The current state for a [StatefulShellRoute]. class StatefulShellRouteState { /// Constructs a [StatefulShellRouteState]. - StatefulShellRouteState( - {required this.route, - required this.navigationBranchState, - required this.currentBranchIndex}); + StatefulShellRouteState({ + required this.route, + required this.navigationBranchState, + required this.currentBranchIndex, + }); /// The associated [StatefulShellRoute] final StatefulShellRoute route; - /// The state for all separate navigation branches associated with a + /// The state for all separate route branches associated with a /// [StatefulShellRoute]. - final List navigationBranchState; + final List navigationBranchState; - /// The index of the currently active navigation branch. + /// The index of the currently active route branch. final int currentBranchIndex; /// Gets the current location from the [topRouteState] or falls back to @@ -107,22 +108,22 @@ class StatefulShellRouteState { navigationBranchState[currentBranchIndex].currentLocation; } -/// The current state for a particular navigation branch -/// ([ShellNavigationBranchItem]) of a [StatefulShellRoute]. -class ShellNavigationBranchState { - /// Constructs a [ShellNavigationBranchState]. - ShellNavigationBranchState({ +/// The current state for a particular route branch +/// ([ShellRouteBranch]) of a [StatefulShellRoute]. +class ShellRouteBranchState { + /// Constructs a [ShellRouteBranchState]. + ShellRouteBranchState({ required this.navigationItem, required this.rootRoutePath, }); - /// The associated [ShellNavigationBranchItem] - final ShellNavigationBranchItem navigationItem; + /// The associated [ShellRouteBranch] + final ShellRouteBranch navigationItem; - /// The full path at which root route for the navigation branch is reachable. + /// The full path at which root route for the route branch is reachable. final String rootRoutePath; - /// The [Navigator] for this navigation branch in a [StatefulShellRoute]. This + /// The [Navigator] for this route branch in a [StatefulShellRoute]. This /// field will typically not be set until this route tree has been navigated /// to at least once. Navigator? navigator; @@ -130,10 +131,14 @@ class ShellNavigationBranchState { /// The [GoRouterState] for the top of the current navigation stack. GoRouterState? topRouteState; - /// Gets the current location from the [topRouteState] or falls back to + /// Gets the defaultLocation specified in [navigationItem] or falls back to /// the root path of the associated [route]. - String get currentLocation => topRouteState?.location ?? rootRoutePath; + String get defaultLocation => navigationItem.defaultLocation ?? rootRoutePath; + + /// Gets the current location from the [topRouteState] or falls back to + /// [defaultLocation]. + String get currentLocation => topRouteState?.location ?? defaultLocation; - /// The root route for the navigation branch. + /// The root route for the route branch. RouteBase get route => navigationItem.rootRoute; } diff --git a/packages/go_router/test/builder_test.dart b/packages/go_router/test/builder_test.dart index 39eabed6fb57..ba553f5ba061 100644 --- a/packages/go_router/test/builder_test.dart +++ b/packages/go_router/test/builder_test.dart @@ -113,7 +113,7 @@ void main() { GlobalKey(debugLabel: 'key'); final RouteConfiguration config = RouteConfiguration( routes: [ - StatefulShellRoute.navigationBranchRoutes( + StatefulShellRoute.rootRoutes( builder: (BuildContext context, GoRouterState state, Widget child) => child, diff --git a/packages/go_router/test/configuration_test.dart b/packages/go_router/test/configuration_test.dart index cab0e28f8ea3..925af78c5aa4 100644 --- a/packages/go_router/test/configuration_test.dart +++ b/packages/go_router/test/configuration_test.dart @@ -97,7 +97,7 @@ void main() { RouteConfiguration( navigatorKey: root, routes: [ - StatefulShellRoute.navigationBranchRoutes( + StatefulShellRoute.rootRoutes( routes: shellRouteChildren, builder: _mockShellBuilder), ], redirectLimit: 10, @@ -126,7 +126,7 @@ void main() { RouteConfiguration( navigatorKey: root, routes: [ - StatefulShellRoute.navigationBranchRoutes( + StatefulShellRoute.rootRoutes( routes: shellRouteChildren, builder: _mockShellBuilder), ], redirectLimit: 10, @@ -161,14 +161,12 @@ void main() { RouteConfiguration( navigatorKey: root, routes: [ - StatefulShellRoute( - navigationBranches: [ - ShellNavigationBranchItem( - rootRoute: routeA, navigatorKey: sectionANavigatorKey), - ShellNavigationBranchItem( - rootRoute: routeB, navigatorKey: sectionBNavigatorKey), - ], - builder: _mockShellBuilder), + StatefulShellRoute(branches: [ + ShellRouteBranch( + rootRoute: routeA, navigatorKey: sectionANavigatorKey), + ShellRouteBranch( + rootRoute: routeB, navigatorKey: sectionBNavigatorKey), + ], builder: _mockShellBuilder), ], redirectLimit: 10, topRedirect: (BuildContext context, GoRouterState state) { diff --git a/packages/go_router/test/go_router_test.dart b/packages/go_router/test/go_router_test.dart index d74824a03941..c102ffb37d8d 100644 --- a/packages/go_router/test/go_router_test.dart +++ b/packages/go_router/test/go_router_test.dart @@ -2417,7 +2417,7 @@ void main() { GlobalKey(); final List routes = [ - StatefulShellRoute.navigationBranchRoutes( + StatefulShellRoute.rootRoutes( builder: (BuildContext context, GoRouterState state, Widget child) => child, routes: [ @@ -2484,7 +2484,7 @@ void main() { GlobalKey(); final List routes = [ - StatefulShellRoute.navigationBranchRoutes( + StatefulShellRoute.rootRoutes( builder: (BuildContext context, GoRouterState state, Widget child) => child, routes: [ @@ -2722,7 +2722,7 @@ void main() { navigatorKey: rootNavigatorKey, initialLocation: '/a', routes: [ - StatefulShellRoute.navigationBranchRoutes( + StatefulShellRoute.rootRoutes( builder: (BuildContext context, GoRouterState state, Widget child) { return Scaffold( From 4c0a91dbe14f5a03b19d2c72626c989b40d70964 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20Lo=CC=88fstrand?= Date: Mon, 10 Oct 2022 14:56:49 +0200 Subject: [PATCH 028/112] Fixed CI analyzer issue. --- packages/go_router/lib/go_router.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/go_router/lib/go_router.dart b/packages/go_router/lib/go_router.dart index 44c0fe9caeb5..859a280ce5f6 100644 --- a/packages/go_router/lib/go_router.dart +++ b/packages/go_router/lib/go_router.dart @@ -11,9 +11,9 @@ export 'src/configuration.dart' GoRoute, GoRouterState, RouteBase, + ShellRoute, ShellRouteBranch, ShellRouteBranchState, - ShellRoute, StatefulShellRoute, StatefulShellRouteState; export 'src/misc/extensions.dart'; From 7556965bc52444afb28678c527292f5b4de8a3b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20Lo=CC=88fstrand?= Date: Wed, 12 Oct 2022 01:47:07 +0200 Subject: [PATCH 029/112] Removed animation support from StatefulNavigationShell and refactored implementation for support for customizing branch navigator container. Refactoring (renaming) and documentation updates. Updated sample code with example of customization with animations. --- .../lib/stateful_nested_navigation.dart | 54 ++++++-- .../src/misc/stateful_navigation_shell.dart | 124 +++++++----------- packages/go_router/lib/src/route.dart | 100 ++++++++------ packages/go_router/lib/src/state.dart | 22 +++- 4 files changed, 166 insertions(+), 134 deletions(-) diff --git a/packages/go_router/example/lib/stateful_nested_navigation.dart b/packages/go_router/example/lib/stateful_nested_navigation.dart index bb95bc30b470..08bb5ece0e54 100644 --- a/packages/go_router/example/lib/stateful_nested_navigation.dart +++ b/packages/go_router/example/lib/stateful_nested_navigation.dart @@ -2,6 +2,7 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; @@ -34,15 +35,15 @@ class NestedTabNavigationExampleApp extends StatelessWidget { final GoRouter _router = GoRouter( initialLocation: '/a', routes: [ - /// Custom top shell route - wraps the below routes in a scaffold with + /// Application shell - wraps the below routes in a scaffold with /// a bottom tab navigator (ScaffoldWithNavBar). Each tab will use its own - /// Navigator, as specified by the navigatorKey for each root route + /// Navigator, as specified by the parentNavigatorKey for each root route /// (branch). For more customization options for the route branches, see /// the default constructor for StatefulShellRoute. StatefulShellRoute.rootRoutes( builder: (BuildContext context, GoRouterState state, - Widget statefulShellNavigation) { - return ScaffoldWithNavBar(body: statefulShellNavigation); + Widget navigationContainer) { + return ScaffoldWithNavBar(body: navigationContainer); }, routes: [ /// The screen to display as the root in the first tab of the bottom @@ -92,11 +93,20 @@ class NestedTabNavigationExampleApp extends StatelessWidget { // (BuildContext context, GoRouterState state, Widget statefulShell) { // return NoTransitionPage(child: statefulShell); // }, - /// To customize shell route branch transitions, provide a transition - /// builder, for example: - // transitionBuilder: - // (BuildContext context, Animation animation, Widget child) => - // FadeTransition(opacity: animation, child: child), + + /// If you need to create a custom container for the branch routes, to + /// for instance setup animations, you can implement your builder + /// something like this: + // builder: + // (BuildContext context, GoRouterState state, Widget ignoringThis) { + // final StatefulShellRouteState shellRouteState = + // StatefulShellRoute.of(context); + // return ScaffoldWithNavBar( + // body: _AnimatedRouteBranchContainer( + // currentIndex: shellRouteState.index, + // navigators: shellRouteState.navigators, + // )); + // }, ), ], ); @@ -136,7 +146,7 @@ class ScaffoldWithNavBar extends StatelessWidget { BottomNavigationBarItem( icon: Icon(Icons.settings), label: 'Section B'), ], - currentIndex: shellState.currentBranchIndex, + currentIndex: shellState.index, onTap: (int tappedIndex) => _onItemTapped( context, shellState.navigationBranchState[tappedIndex], @@ -146,7 +156,7 @@ class ScaffoldWithNavBar extends StatelessWidget { } void _onItemTapped(BuildContext context, ShellRouteBranchState routeState) { - GoRouter.of(context).go(routeState.currentLocation); + GoRouter.of(context).go(routeState.location); } } @@ -257,3 +267,25 @@ class DetailsScreenState extends State { ); } } + +// ignore: unused_element +class _AnimatedRouteBranchContainer extends StatelessWidget { + const _AnimatedRouteBranchContainer( + {Key? key, required this.currentIndex, required this.navigators}) + : super(key: key); + + final int currentIndex; + final List navigators; + + @override + Widget build(BuildContext context) { + return Stack( + children: navigators.mapIndexed((int index, Widget? navigator) { + return AnimatedOpacity( + opacity: index == currentIndex ? 1 : 0, + duration: const Duration(milliseconds: 400), + child: navigator ?? const SizedBox.shrink(), + ); + }).toList()); + } +} diff --git a/packages/go_router/lib/src/misc/stateful_navigation_shell.dart b/packages/go_router/lib/src/misc/stateful_navigation_shell.dart index e22addd8bdf1..7a9c5a210f71 100644 --- a/packages/go_router/lib/src/misc/stateful_navigation_shell.dart +++ b/packages/go_router/lib/src/misc/stateful_navigation_shell.dart @@ -8,18 +8,6 @@ import 'package:flutter/widgets.dart'; import '../configuration.dart'; import '../typedefs.dart'; -/// Transition builder callback used by [StatefulNavigationShell]. -/// -/// The builder is expected to return a transition powered by the provided -/// `animation` and wrapping the provided `child`. -/// -/// The `animation` provided to the builder always runs forward from 0.0 to 1.0. -typedef StatefulNavigationTransitionBuilder = Widget Function( - BuildContext context, - Animation animation, - Widget child, -); - /// [InheritedWidget] for providing a reference to the closest /// [StatefulNavigationShellState]. class InheritedStatefulNavigationShell extends InheritedWidget { @@ -33,6 +21,9 @@ class InheritedStatefulNavigationShell extends InheritedWidget { /// The [StatefulNavigationShellState] that is exposed by this InheritedWidget. final StatefulNavigationShellState state; + /// The [StatefulShellRouteState] that is exposed by this InheritedWidget. + StatefulShellRouteState get routeState => state.routeState; + @override bool updateShouldNotify( covariant InheritedStatefulNavigationShell oldWidget) { @@ -40,17 +31,21 @@ class InheritedStatefulNavigationShell extends InheritedWidget { } } -/// Widget that maintains a stateful stack of [Navigator]s, using an -/// [IndexedStack]. +/// Widget that manages and maintains the state of a [StatefulShellRoute], +/// including the [Navigator]s of the configured route branches. /// -/// Each item in the stack is represented by a [StackedNavigationItem], -/// specified in the `stackItems` parameter. The stack items will be used to -/// build the widgets containing the [Navigator] for each index in the stack. -/// Once a stack item (along with its Navigator) has been initialized, it will -/// remain in a widget tree, wrapped in an [Offstage] widget. +/// This widget acts as a wrapper around the builder function specified for the +/// associated StatefulShellRoute, and exposes the state (represented by +/// [StatefulShellRouteState]) to its child widgets with the help of the +/// InheritedWidget [InheritedStatefulNavigationShell]. The state for each route +/// branch is represented by [ShellRouteBranchState] and can be accessed via the +/// StatefulShellRouteState. /// -/// The stacked navigation shell can be customized by specifying a -/// `scaffoldBuilder`, to build a widget that wraps the index stack. +/// By default, this widget creates a container for the branch route Navigators, +/// provided as the child argument to the builder of the StatefulShellRoute. +/// However, implementors can choose to disregard this and use an alternate +/// container around the branch navigators +/// (see [StatefulShellRouteState.navigators]) instead. class StatefulNavigationShell extends StatefulWidget { /// Constructs an [StatefulNavigationShell]. const StatefulNavigationShell({ @@ -62,9 +57,6 @@ class StatefulNavigationShell extends StatefulWidget { super.key, }); - /// The default transition duration - static const Duration defaultTransitionDuration = Duration(milliseconds: 400); - /// The route configuration for the app. final RouteConfiguration configuration; @@ -85,19 +77,11 @@ class StatefulNavigationShell extends StatefulWidget { } /// State for StatefulNavigationShell. -class StatefulNavigationShellState extends State - with SingleTickerProviderStateMixin { +class StatefulNavigationShellState extends State { int _currentIndex = 0; - late final AnimationController? _animationController; - late final List _childRouteState; - StatefulNavigationTransitionBuilder? get _transitionBuilder => - widget.shellRoute.transitionBuilder; - - Duration? get _transitionDuration => widget.shellRoute.transitionDuration; - int _findCurrentIndex() { final int index = _childRouteState.indexWhere((ShellRouteBranchState i) => i.navigationItem.navigatorKey == widget.activeNavigator.key); @@ -106,9 +90,10 @@ class StatefulNavigationShellState extends State /// The current [StatefulShellRouteState] StatefulShellRouteState get routeState => StatefulShellRouteState( - route: widget.shellRoute, - navigationBranchState: _childRouteState, - currentBranchIndex: _currentIndex); + route: widget.shellRoute, + navigationBranchState: _childRouteState, + index: _currentIndex, + ); @override void initState() { @@ -119,16 +104,6 @@ class StatefulNavigationShellState extends State rootRoutePath: widget.configuration.fullPathForRoute(e.rootRoute), )) .toList(); - - if (_transitionBuilder != null) { - _animationController = AnimationController( - vsync: this, - duration: _transitionDuration ?? - StatefulNavigationShell.defaultTransitionDuration); - _animationController?.forward(); - } else { - _animationController = null; - } } @override @@ -144,22 +119,16 @@ class StatefulNavigationShellState extends State } void _updateForCurrentTab() { - final int previousIndex = _currentIndex; _currentIndex = _findCurrentIndex(); - final ShellRouteBranchState itemState = _childRouteState[_currentIndex]; - itemState.navigator = widget.activeNavigator; - itemState.topRouteState = widget.topRouterState; - - if (previousIndex != _currentIndex) { - _animationController?.forward(from: 0.0); - } - } - - @override - void dispose() { - _animationController?.dispose(); - super.dispose(); + final ShellRouteBranchState currentBranchState = + _childRouteState[_currentIndex]; + _childRouteState[_currentIndex] = ShellRouteBranchState( + navigationItem: currentBranchState.navigationItem, + rootRoutePath: currentBranchState.rootRoutePath, + navigator: widget.activeNavigator, + topRouteState: widget.topRouterState, + ); } @override @@ -171,37 +140,40 @@ class StatefulNavigationShellState extends State return builder( context, widget.shellRouterState, - _buildIndexStack(context), + _IndexedStackedRouteBranchContainer( + branchState: _childRouteState, currentIndex: _currentIndex), ); }), ); } +} + +/// Default implementation of a container widget for the [Navigator]s of the +/// route branches. This implementation uses an [IndexedStack] as a container. +class _IndexedStackedRouteBranchContainer extends StatelessWidget { + const _IndexedStackedRouteBranchContainer( + {required this.currentIndex, required this.branchState}); - Widget _buildIndexStack(BuildContext context) { - final List children = _childRouteState + final int currentIndex; + final List branchState; + + @override + Widget build(BuildContext context) { + final List children = branchState .mapIndexed((int index, ShellRouteBranchState item) => - _buildNavigator(context, index, item)) + _buildRouteBranchContainer(context, index, item)) .toList(); - final Widget indexedStack = - IndexedStack(index: _currentIndex, children: children); - - final StatefulNavigationTransitionBuilder? transitionBuilder = - _transitionBuilder; - if (transitionBuilder != null) { - return transitionBuilder(context, _animationController!, indexedStack); - } else { - return indexedStack; - } + return IndexedStack(index: currentIndex, children: children); } - Widget _buildNavigator( + Widget _buildRouteBranchContainer( BuildContext context, int index, ShellRouteBranchState navigationItem) { final Navigator? navigator = navigationItem.navigator; if (navigator == null) { return const SizedBox.shrink(); } - final bool isActive = index == _currentIndex; + final bool isActive = index == currentIndex; return Offstage( offstage: !isActive, child: TickerMode( diff --git a/packages/go_router/lib/src/route.dart b/packages/go_router/lib/src/route.dart index 518340e1f379..6e73890887df 100644 --- a/packages/go_router/lib/src/route.dart +++ b/packages/go_router/lib/src/route.dart @@ -493,18 +493,20 @@ class ShellRoute extends ShellRouteBase { /// with a [BottomNavigationBar], with a persistent navigation state for each /// tab. /// -/// To access the current state of this route, to for instance access the -/// index of the current route branch - use the method -/// [StatefulShellRoute.of]. For example: -/// -/// ``` -/// final StatefulShellRouteState shellState = StatefulShellRoute.of(context); -/// ``` +/// A StatefulShellRoute is created by specifying a List of [ShellRouteBranch] +/// items, each representing a route branch, in form of a navigator key and a +/// root route. The navigator key ([GlobalKey]) identifies which Navigator will +/// be used for the route branch. There is also a simpler shorthand way of +/// creating a StatefulShellRoute by using a List of child [GoRoute]s, each +/// representing the root route of each route branch. When using this shorthand +/// constructor, a parentNavigatorKey need also be specified for each root +/// GoRoute. /// /// Below is a simple example of how a router configuration with /// StatefulShellRoute could be achieved. In this example, a /// BottomNavigationBar with two tabs is used, and each of the tabs gets its -/// own Navigator. This Navigator will then be passed as the child argument +/// own Navigator. A container widget responsible for managing the Navigators +/// for all route branches will then be passed as the child argument /// of the [builder] function. /// /// ``` @@ -516,15 +518,15 @@ class ShellRoute extends ShellRouteBase { /// final GoRouter _router = GoRouter( /// initialLocation: '/a', /// routes: [ -/// StatefulShellRoute.branchRootRoutes( +/// StatefulShellRoute.rootRoutes( /// builder: (BuildContext context, GoRouterState state, -/// Widget statefulShellNavigation) { -/// return ScaffoldWithNavBar(body: statefulShellNavigation); +/// Widget navigationContainer) { +/// return ScaffoldWithNavBar(body: navigationContainer); /// }, /// routes: [ /// /// The first branch, i.e. root of tab 'A' -/// ShellBranchRoute( -/// navigatorKey: _tabANavigatorKey, +/// GoRoute( +/// parentNavigatorKey: _tabANavigatorKey, /// path: '/a', /// builder: (BuildContext context, GoRouterState state) => /// const RootScreen(label: 'A', detailsPath: '/a/details'), @@ -538,8 +540,8 @@ class ShellRoute extends ShellRouteBase { /// ], /// ), /// /// The second branch, i.e. root of tab 'B' -/// ShellBranchRoute( -/// navigatorKey: _tabBNavigatorKey, +/// GoRoute( +/// parentNavigatorKey: _tabBNavigatorKey, /// path: '/b', /// builder: (BuildContext context, GoRouterState state) => /// const RootScreen(label: 'B', detailsPath: '/b/details/1'), @@ -568,10 +570,10 @@ class ShellRoute extends ShellRouteBase { /// final GoRouter _router = GoRouter( /// initialLocation: '/a', /// routes: [ -/// StatefulShellRoute.branchRootRoutes( +/// StatefulShellRoute.rootRoutes( /// builder: (BuildContext context, GoRouterState state, -/// Widget statefulShellNavigation) { -/// return ScaffoldWithNavBar(body: statefulShellNavigation); +/// Widget navigationContainer) { +/// return ScaffoldWithNavBar(body: navigationContainer); /// }, /// pageProvider: /// (BuildContext context, GoRouterState state, Widget statefulShell) { @@ -580,12 +582,14 @@ class ShellRoute extends ShellRouteBase { /// routes: [ /// /// The first branch, i.e. root of tab 'A' /// GoRoute( +/// parentNavigatorKey: _tabANavigatorKey, /// path: '/a', /// builder: (BuildContext context, GoRouterState state) => /// const RootScreen(label: 'A', detailsPath: '/a/details'), /// ), /// /// The second branch, i.e. root of tab 'B' /// GoRoute( +/// parentNavigatorKey: _tabBNavigatorKey, /// path: '/b', /// builder: (BuildContext context, GoRouterState state) => /// const RootScreen(label: 'B', detailsPath: '/b/details/1'), @@ -596,8 +600,16 @@ class ShellRoute extends ShellRouteBase { /// ); /// ``` /// +/// To access the current state of this route, to for instance access the +/// index of the current route branch - use the method +/// [StatefulShellRoute.of]. For example: +/// +/// ``` +/// final StatefulShellRouteState shellState = StatefulShellRoute.of(context); +/// ``` +/// /// See [Stateful Nested Navigation](https://github.com/flutter/packages/blob/main/packages/go_router/example/lib/stateful_nested_navigation.dart) -/// for a complete runnable example. +/// for a complete runnable example using StatefulShellRoute. class StatefulShellRoute extends ShellRouteBase { /// Constructs a [StatefulShellRoute] from a list of [ShellRouteBranch], each /// representing a root in a stateful route branch. @@ -608,8 +620,6 @@ class StatefulShellRoute extends ShellRouteBase { required this.branches, required this.builder, this.pageProvider, - this.transitionBuilder, - this.transitionDuration, }) : assert(branches.isNotEmpty), assert(_uniqueNavigatorKeys(branches).length == branches.length, 'Navigator keys must be unique'), @@ -618,7 +628,7 @@ class StatefulShellRoute extends ShellRouteBase { final RouteBase route = routes[i]; if (route is GoRoute) { assert(route.parentNavigatorKey == null || - route.parentNavigatorKey == navigatorKeys[i]); + route.parentNavigatorKey == branches[i].navigatorKey); } } } @@ -635,8 +645,6 @@ class StatefulShellRoute extends ShellRouteBase { required List routes, required ShellRouteBuilder builder, ShellRoutePageBuilder? pageProvider, - StatefulNavigationTransitionBuilder? transitionBuilder, - Duration? transitionDuration, }) : this( branches: routes.map((GoRoute e) { final GlobalKey? key = e.parentNavigatorKey; @@ -645,8 +653,6 @@ class StatefulShellRoute extends ShellRouteBase { }).toList(), builder: builder, pageProvider: pageProvider, - transitionBuilder: transitionBuilder, - transitionDuration: transitionDuration, ); /// Representations of the different stateful route branches that this @@ -658,11 +664,29 @@ class StatefulShellRoute extends ShellRouteBase { /// The widget builder for a stateful shell route. /// /// Similar to GoRoute builder, but with an additional child parameter. This - /// child parameter is the Widget responsible for managing - and switching - /// between - the navigators for the different branches of this - /// StatefulShellRoute. The Widget returned by this function will be embedded - /// into the stateful shell, making it possible to access the - /// [StatefulShellRouteState] via the method [StatefulShellRoute.of]. + /// child parameter is a Widget that contains - and is responsible for + /// managing - the Navigators for the different route branches of this + /// StatefulShellRoute. This widget is meant to be used as the body of a + /// custom shell implementation, for example as the body of [Scaffold] with a + /// [BottomNavigationBar]. + /// + /// The builder function of a StatefulShellRoute will be invoked from within a + /// wrapper Widget that provides access to the current + /// [StatefulShellRouteState] associated with the route (via the method + /// [StatefulShellRoute.of]). That state object exposes information such as + /// the current branch index, the state of the route branches etc. + /// + /// For implementations where greater control is needed over the layout and + /// animations of the Navigators, the child parameter can be ignored, and a + /// custom implementation can instead be built by using the Navigators (and + /// other information from StatefulShellRouteState) directly. For example: + /// + /// ``` + /// final StatefulShellRouteState shellState = StatefulShellRoute.of(context); + /// final int currentIndex = shellState.currentBranchIndex; + /// final List navigators = shellRouteState.branchNavigators; + /// return MyCustomShell(currentIndex, navigators); + /// ``` final ShellRouteBuilder builder; /// Function for customizing the [Page] for this stateful shell. @@ -673,14 +697,10 @@ class StatefulShellRoute extends ShellRouteBase { /// built for this route, using the builder function. final ShellRoutePageBuilder? pageProvider; - /// An optional transition builder for transitions when switching navigation - /// branch in the shell. - final StatefulNavigationTransitionBuilder? transitionBuilder; - - /// The duration for shell transitions - final Duration? transitionDuration; - - /// The navigator keys of the navigators created by this route. + /// The [GlobalKey]s to be used by the [Navigator]s built for this route. + /// StatefulShellRoute builds a Navigator for each of its route branches. + /// Routes on a particular branch are then placed onto these Navigators + /// instead of the root Navigator. List> get navigatorKeys => branches.map((ShellRouteBranch e) => e.navigatorKey).toList(); @@ -699,7 +719,7 @@ class StatefulShellRoute extends ShellRouteBase { .dependOnInheritedWidgetOfExactType(); assert(inherited != null, 'No InheritedStatefulNavigationShell found in context'); - return inherited!.state.routeState; + return inherited!.routeState; } static Set> _uniqueNavigatorKeys( diff --git a/packages/go_router/lib/src/state.dart b/packages/go_router/lib/src/state.dart index e369eceb8373..fee8e1959ed4 100644 --- a/packages/go_router/lib/src/state.dart +++ b/packages/go_router/lib/src/state.dart @@ -89,7 +89,7 @@ class StatefulShellRouteState { StatefulShellRouteState({ required this.route, required this.navigationBranchState, - required this.currentBranchIndex, + required this.index, }); /// The associated [StatefulShellRoute] @@ -100,12 +100,18 @@ class StatefulShellRouteState { final List navigationBranchState; /// The index of the currently active route branch. - final int currentBranchIndex; + final int index; /// Gets the current location from the [topRouteState] or falls back to /// the root path of the associated [route]. - String get currentLocation => - navigationBranchState[currentBranchIndex].currentLocation; + String get location => navigationBranchState[index].location; + + /// Gets the [Navigator]s for each of the route branches. Note that the + /// Navigator for a particular branch may be null if the branch hasn't been + /// visited yet. + List get navigators => navigationBranchState + .map((ShellRouteBranchState e) => e.navigator) + .toList(); } /// The current state for a particular route branch @@ -115,6 +121,8 @@ class ShellRouteBranchState { ShellRouteBranchState({ required this.navigationItem, required this.rootRoutePath, + this.navigator, + this.topRouteState, }); /// The associated [ShellRouteBranch] @@ -126,10 +134,10 @@ class ShellRouteBranchState { /// The [Navigator] for this route branch in a [StatefulShellRoute]. This /// field will typically not be set until this route tree has been navigated /// to at least once. - Navigator? navigator; + final Navigator? navigator; /// The [GoRouterState] for the top of the current navigation stack. - GoRouterState? topRouteState; + final GoRouterState? topRouteState; /// Gets the defaultLocation specified in [navigationItem] or falls back to /// the root path of the associated [route]. @@ -137,7 +145,7 @@ class ShellRouteBranchState { /// Gets the current location from the [topRouteState] or falls back to /// [defaultLocation]. - String get currentLocation => topRouteState?.location ?? defaultLocation; + String get location => topRouteState?.location ?? defaultLocation; /// The root route for the route branch. RouteBase get route => navigationItem.rootRoute; From 59a6b0578560640ecbef17d6077ce55b6cb9ceb9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20Lo=CC=88fstrand?= Date: Thu, 13 Oct 2022 13:54:28 +0200 Subject: [PATCH 030/112] Updated changelog (replaced PartitionedShellRoute with StatefulShellRoute). --- packages/go_router/CHANGELOG.md | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/go_router/CHANGELOG.md b/packages/go_router/CHANGELOG.md index 46e34728fbcb..e47491a75463 100644 --- a/packages/go_router/CHANGELOG.md +++ b/packages/go_router/CHANGELOG.md @@ -1,9 +1,8 @@ ## 5.1.0 -- Introduced a new shell route class called `PartitionedShellRoute`, to support using separate +- Introduced a new shell route class called `StatefulShellRoute`, to support using separate navigators for child routes as well as preserving state in each navigation tree - (flutter/flutter#99124). Also introduced the supporting widget class `StackedNavigationShell`, - which facilitates using an `IndexStack` to manage multiple parallel navigation trees. + (flutter/flutter#99124). - Updated documentation for `pageBuilder` and `builder` fields of `ShellRoute`, to more correctly describe the meaning of the child argument in the builder functions. From 1b877c84faf089de934383068fae66760a235da6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20Lo=CC=88fstrand?= Date: Sat, 22 Oct 2022 13:53:39 +0200 Subject: [PATCH 031/112] Implemented equality in StatefulShellRouteState and ShellRouteBranchState. Updated the way StatefulNavigationShell manages state. Some renaming of fields. --- .../lib/stateful_nested_navigation.dart | 2 +- packages/go_router/lib/src/builder.dart | 4 +- .../src/misc/stateful_navigation_shell.dart | 90 +++++++++---------- packages/go_router/lib/src/state.dart | 84 ++++++++++++----- 4 files changed, 109 insertions(+), 71 deletions(-) diff --git a/packages/go_router/example/lib/stateful_nested_navigation.dart b/packages/go_router/example/lib/stateful_nested_navigation.dart index 08bb5ece0e54..5c9990000912 100644 --- a/packages/go_router/example/lib/stateful_nested_navigation.dart +++ b/packages/go_router/example/lib/stateful_nested_navigation.dart @@ -149,7 +149,7 @@ class ScaffoldWithNavBar extends StatelessWidget { currentIndex: shellState.index, onTap: (int tappedIndex) => _onItemTapped( context, - shellState.navigationBranchState[tappedIndex], + shellState.branchState[tappedIndex], ), ), ); diff --git a/packages/go_router/lib/src/builder.dart b/packages/go_router/lib/src/builder.dart index bfeb7257211a..5b02731be9be 100644 --- a/packages/go_router/lib/src/builder.dart +++ b/packages/go_router/lib/src/builder.dart @@ -272,8 +272,8 @@ class RouteBuilder { configuration: configuration, shellRoute: shellRoute, activeNavigator: navigator, - shellRouterState: shellRouterState, - topRouterState: topRouterState, + shellGoRouterState: shellRouterState, + topGoRouterState: topRouterState, ); } diff --git a/packages/go_router/lib/src/misc/stateful_navigation_shell.dart b/packages/go_router/lib/src/misc/stateful_navigation_shell.dart index 7a9c5a210f71..3f6b91a17b28 100644 --- a/packages/go_router/lib/src/misc/stateful_navigation_shell.dart +++ b/packages/go_router/lib/src/misc/stateful_navigation_shell.dart @@ -14,20 +14,17 @@ class InheritedStatefulNavigationShell extends InheritedWidget { /// Constructs an [InheritedStatefulNavigationShell]. const InheritedStatefulNavigationShell({ required super.child, - required this.state, + required this.routeState, super.key, }); - /// The [StatefulNavigationShellState] that is exposed by this InheritedWidget. - final StatefulNavigationShellState state; - /// The [StatefulShellRouteState] that is exposed by this InheritedWidget. - StatefulShellRouteState get routeState => state.routeState; + final StatefulShellRouteState routeState; @override bool updateShouldNotify( covariant InheritedStatefulNavigationShell oldWidget) { - return state != oldWidget.state; + return routeState != oldWidget.routeState; } } @@ -52,8 +49,8 @@ class StatefulNavigationShell extends StatefulWidget { required this.configuration, required this.shellRoute, required this.activeNavigator, - required this.shellRouterState, - required this.topRouterState, + required this.shellGoRouterState, + required this.topGoRouterState, super.key, }); @@ -67,10 +64,10 @@ class StatefulNavigationShell extends StatefulWidget { final Navigator activeNavigator; /// The [GoRouterState] for navigation shell. - final GoRouterState shellRouterState; + final GoRouterState shellGoRouterState; /// The [GoRouterState] for the top of the current navigation stack. - final GoRouterState topRouterState; + final GoRouterState topGoRouterState; @override State createState() => StatefulNavigationShellState(); @@ -78,32 +75,29 @@ class StatefulNavigationShell extends StatefulWidget { /// State for StatefulNavigationShell. class StatefulNavigationShellState extends State { - int _currentIndex = 0; - - late final List _childRouteState; + late StatefulShellRouteState _routeState; int _findCurrentIndex() { - final int index = _childRouteState.indexWhere((ShellRouteBranchState i) => - i.navigationItem.navigatorKey == widget.activeNavigator.key); + final List branchState = _routeState.branchState; + final int index = branchState.indexWhere((ShellRouteBranchState e) => + e.routeBranch.navigatorKey == widget.activeNavigator.key); return index < 0 ? 0 : index; } - /// The current [StatefulShellRouteState] - StatefulShellRouteState get routeState => StatefulShellRouteState( - route: widget.shellRoute, - navigationBranchState: _childRouteState, - index: _currentIndex, - ); - @override void initState() { super.initState(); - _childRouteState = widget.shellRoute.branches + final List branchState = widget.shellRoute.branches .map((ShellRouteBranch e) => ShellRouteBranchState( - navigationItem: e, + routeBranch: e, rootRoutePath: widget.configuration.fullPathForRoute(e.rootRoute), )) .toList(); + _routeState = StatefulShellRouteState( + route: widget.shellRoute, + branchState: branchState, + index: 0, + ); } @override @@ -119,29 +113,37 @@ class StatefulNavigationShellState extends State { } void _updateForCurrentTab() { - _currentIndex = _findCurrentIndex(); + final int currentIndex = _findCurrentIndex(); - final ShellRouteBranchState currentBranchState = - _childRouteState[_currentIndex]; - _childRouteState[_currentIndex] = ShellRouteBranchState( - navigationItem: currentBranchState.navigationItem, + final List branchState = + _routeState.branchState.toList(); + final ShellRouteBranchState currentBranchState = branchState[currentIndex]; + branchState[currentIndex] = ShellRouteBranchState( + routeBranch: currentBranchState.routeBranch, rootRoutePath: currentBranchState.rootRoutePath, navigator: widget.activeNavigator, - topRouteState: widget.topRouterState, + topGoRouterState: widget.topGoRouterState, + ); + + _routeState = StatefulShellRouteState( + route: widget.shellRoute, + branchState: branchState, + index: currentIndex, ); } @override Widget build(BuildContext context) { return InheritedStatefulNavigationShell( - state: this, + routeState: _routeState, child: Builder(builder: (BuildContext context) { - final ShellRouteBuilder builder = widget.shellRoute.builder; - return builder( + // This Builder Widget is mainly used to make it possible to access the + // StatefulShellRouteState via the BuildContext in the ShellRouteBuilder + final ShellRouteBuilder shellRouteBuilder = widget.shellRoute.builder; + return shellRouteBuilder( context, - widget.shellRouterState, - _IndexedStackedRouteBranchContainer( - branchState: _childRouteState, currentIndex: _currentIndex), + widget.shellGoRouterState, + _IndexedStackedRouteBranchContainer(routeState: _routeState), ); }), ); @@ -151,29 +153,27 @@ class StatefulNavigationShellState extends State { /// Default implementation of a container widget for the [Navigator]s of the /// route branches. This implementation uses an [IndexedStack] as a container. class _IndexedStackedRouteBranchContainer extends StatelessWidget { - const _IndexedStackedRouteBranchContainer( - {required this.currentIndex, required this.branchState}); + const _IndexedStackedRouteBranchContainer({required this.routeState}); - final int currentIndex; - final List branchState; + final StatefulShellRouteState routeState; @override Widget build(BuildContext context) { - final List children = branchState + final List children = routeState.branchState .mapIndexed((int index, ShellRouteBranchState item) => _buildRouteBranchContainer(context, index, item)) .toList(); - return IndexedStack(index: currentIndex, children: children); + return IndexedStack(index: routeState.index, children: children); } Widget _buildRouteBranchContainer( - BuildContext context, int index, ShellRouteBranchState navigationItem) { - final Navigator? navigator = navigationItem.navigator; + BuildContext context, int index, ShellRouteBranchState routeBranch) { + final Navigator? navigator = routeBranch.navigator; if (navigator == null) { return const SizedBox.shrink(); } - final bool isActive = index == currentIndex; + final bool isActive = index == routeState.index; return Offstage( offstage: !isActive, child: TickerMode( diff --git a/packages/go_router/lib/src/state.dart b/packages/go_router/lib/src/state.dart index fee8e1959ed4..e82dd419bf0f 100644 --- a/packages/go_router/lib/src/state.dart +++ b/packages/go_router/lib/src/state.dart @@ -2,6 +2,7 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'package:flutter/foundation.dart'; import 'package:flutter/widgets.dart'; import 'configuration.dart'; @@ -84,11 +85,12 @@ class GoRouterState { } /// The current state for a [StatefulShellRoute]. +@immutable class StatefulShellRouteState { /// Constructs a [StatefulShellRouteState]. - StatefulShellRouteState({ + const StatefulShellRouteState({ required this.route, - required this.navigationBranchState, + required this.branchState, required this.index, }); @@ -97,36 +99,54 @@ class StatefulShellRouteState { /// The state for all separate route branches associated with a /// [StatefulShellRoute]. - final List navigationBranchState; + final List branchState; /// The index of the currently active route branch. final int index; - /// Gets the current location from the [topRouteState] or falls back to - /// the root path of the associated [route]. - String get location => navigationBranchState[index].location; - - /// Gets the [Navigator]s for each of the route branches. Note that the - /// Navigator for a particular branch may be null if the branch hasn't been - /// visited yet. - List get navigators => navigationBranchState - .map((ShellRouteBranchState e) => e.navigator) - .toList(); + /// Gets the location of the current branch. + /// + /// See [ShellRouteBranchState.location] for more details. + String get location => branchState[index].location; + + /// Gets the [Navigator]s for each of the route branches. + /// + /// Note that the Navigator for a particular branch may be null if the branch + /// hasn't been visited yet. + List get navigators => + branchState.map((ShellRouteBranchState e) => e.navigator).toList(); + + @override + bool operator ==(Object other) { + if (identical(other, this)) { + return true; + } + if (other is! StatefulShellRouteState) { + return false; + } + return other.route == route && + listEquals(other.branchState, branchState) && + other.index == index; + } + + @override + int get hashCode => Object.hash(route, branchState, index); } /// The current state for a particular route branch /// ([ShellRouteBranch]) of a [StatefulShellRoute]. +@immutable class ShellRouteBranchState { /// Constructs a [ShellRouteBranchState]. - ShellRouteBranchState({ - required this.navigationItem, + const ShellRouteBranchState({ + required this.routeBranch, required this.rootRoutePath, this.navigator, - this.topRouteState, + this.topGoRouterState, }); /// The associated [ShellRouteBranch] - final ShellRouteBranch navigationItem; + final ShellRouteBranch routeBranch; /// The full path at which root route for the route branch is reachable. final String rootRoutePath; @@ -137,16 +157,34 @@ class ShellRouteBranchState { final Navigator? navigator; /// The [GoRouterState] for the top of the current navigation stack. - final GoRouterState? topRouteState; + final GoRouterState? topGoRouterState; - /// Gets the defaultLocation specified in [navigationItem] or falls back to + /// Gets the defaultLocation specified in [routeBranch] or falls back to /// the root path of the associated [route]. - String get defaultLocation => navigationItem.defaultLocation ?? rootRoutePath; + String get defaultLocation => routeBranch.defaultLocation ?? rootRoutePath; - /// Gets the current location from the [topRouteState] or falls back to + /// Gets the current location from the [topGoRouterState] or falls back to /// [defaultLocation]. - String get location => topRouteState?.location ?? defaultLocation; + String get location => topGoRouterState?.location ?? defaultLocation; /// The root route for the route branch. - RouteBase get route => navigationItem.rootRoute; + RouteBase? get route => routeBranch.rootRoute; + + @override + bool operator ==(Object other) { + if (identical(other, this)) { + return true; + } + if (other is! ShellRouteBranchState) { + return false; + } + return other.routeBranch == routeBranch && + other.rootRoutePath == rootRoutePath && + other.navigator == navigator && + other.topGoRouterState == topGoRouterState; + } + + @override + int get hashCode => + Object.hash(routeBranch, rootRoutePath, navigator, topGoRouterState); } From e542a050d516d6513f4afbaa3a7f32b1566dade0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20Lo=CC=88fstrand?= Date: Sun, 23 Oct 2022 21:50:57 +0200 Subject: [PATCH 032/112] Documentation updates. Documentation and api changes related to replacing the term "child route" with "sub-route". Removed the need for providing Navigator keys for branches in StatefulShellRoute (falling back to default value in ShellRouteBranch). Renamed field pageProvider to pageBuilder in StatefulShellRoute. --- packages/go_router/lib/src/builder.dart | 4 +- packages/go_router/lib/src/delegate.dart | 4 +- packages/go_router/lib/src/route.dart | 202 +++++++++++++---------- 3 files changed, 121 insertions(+), 89 deletions(-) diff --git a/packages/go_router/lib/src/builder.dart b/packages/go_router/lib/src/builder.dart index 5b02731be9be..b4af89886bd3 100644 --- a/packages/go_router/lib/src/builder.dart +++ b/packages/go_router/lib/src/builder.dart @@ -188,7 +188,7 @@ class RouteBuilder { // The key to provide to the shell route's Navigator. final GlobalKey? shellNavigatorKey = - route.navigatorKeyForChildRoute(childRoute); + route.navigatorKeyForSubRoute(childRoute); if (shellNavigatorKey == null) { throw _RouteBuilderError( 'Shell routes must always have a navigator key'); @@ -318,7 +318,7 @@ class RouteBuilder { page = pageBuilder(context, state); } } else if (route is StatefulShellRoute) { - final ShellRoutePageBuilder? pageForShell = route.pageProvider; + final ShellRoutePageBuilder? pageForShell = route.pageBuilder; assert(child != null, 'StatefulShellRoute must contain a child route'); if (pageForShell != null) { page = pageForShell(context, state, child!); diff --git a/packages/go_router/lib/src/delegate.dart b/packages/go_router/lib/src/delegate.dart index 38efaaf130bc..9b6329743c29 100644 --- a/packages/go_router/lib/src/delegate.dart +++ b/packages/go_router/lib/src/delegate.dart @@ -71,7 +71,7 @@ class GoRouterDelegate extends RouterDelegate // For shell routes, find the navigator key that should be used for the // child route in the current match list final GlobalKey? navigatorKey = - route.navigatorKeyForChildRoute(childRoute); + route.navigatorKeyForSubRoute(childRoute); final bool didPop = await navigatorKey?.currentState!.maybePop() ?? false; @@ -141,7 +141,7 @@ class GoRouterDelegate extends RouterDelegate // For shell routes, find the navigator key that should be used for the // child route in the current match list final GlobalKey? navigatorKey = - route.navigatorKeyForChildRoute(childRoute); + route.navigatorKeyForSubRoute(childRoute); final bool canPop = navigatorKey?.currentState!.canPop() ?? false; diff --git a/packages/go_router/lib/src/route.dart b/packages/go_router/lib/src/route.dart index 6e73890887df..75fff9a3600b 100644 --- a/packages/go_router/lib/src/route.dart +++ b/packages/go_router/lib/src/route.dart @@ -334,14 +334,17 @@ class GoRoute extends RouteBase { late final RegExp _pathRE; } -/// Base class for classes that acts as a shell for child routes, such +/// Base class for classes that acts as a shell for sub-routes, such /// as [ShellRoute] and [StatefulShellRoute]. abstract class ShellRouteBase extends RouteBase { const ShellRouteBase._({super.routes}) : super._(); /// Returns the key for the [Navigator] that is to be used for the specified - /// child route. - GlobalKey? navigatorKeyForChildRoute(RouteBase route); + /// sub-route. + /// + /// The route passed as an argument to this method must be an immediate + /// sub-route to this shell route. + GlobalKey? navigatorKeyForSubRoute(RouteBase subRoute); } /// A route that displays a UI shell around the matching child route. @@ -477,15 +480,18 @@ class ShellRoute extends ShellRouteBase { final GlobalKey navigatorKey; @override - GlobalKey? navigatorKeyForChildRoute(RouteBase route) { - return navigatorKey; + GlobalKey? navigatorKeyForSubRoute(RouteBase subRoute) { + if (routes.contains(subRoute)) { + return navigatorKey; + } + return null; } } -/// A route that displays a UI shell with separate [Navigator]s for its child -/// routes. +/// A route that displays a UI shell with separate [Navigator]s for its +/// sub-routes. /// -/// Similar to [ShellRoute], this route class places its sub routes on a +/// Similar to [ShellRoute], this route class places its sub-route on a /// different Navigator than the root Navigator. However, this route class /// differs in that it creates separate Navigators for each of its nested /// route branches (route trees), making it possible to build a stateful @@ -494,13 +500,13 @@ class ShellRoute extends ShellRouteBase { /// tab. /// /// A StatefulShellRoute is created by specifying a List of [ShellRouteBranch] -/// items, each representing a route branch, in form of a navigator key and a -/// root route. The navigator key ([GlobalKey]) identifies which Navigator will -/// be used for the route branch. There is also a simpler shorthand way of -/// creating a StatefulShellRoute by using a List of child [GoRoute]s, each -/// representing the root route of each route branch. When using this shorthand -/// constructor, a parentNavigatorKey need also be specified for each root -/// GoRoute. +/// items, each representing a separate stateful branch in the route tree. +/// ShellRouteBranch provides the root route and the key [GlobalKey] for the +/// Navigator of branch, as well as an optional default location. There is also +/// a simpler shorthand way of creating a StatefulShellRoute by using a List of +/// [GoRoute]s, each representing the root route of a route branch. When using +/// this shorthand constructor, the [GoRoute.parentNavigatorKey] will be used +/// as the Navigator key. /// /// Below is a simple example of how a router configuration with /// StatefulShellRoute could be achieved. In this example, a @@ -514,45 +520,49 @@ class ShellRoute extends ShellRouteBase { /// GlobalKey(debugLabel: 'tabANavigator'); /// final GlobalKey _tabBNavigatorKey = /// GlobalKey(debugLabel: 'tabBNavigator'); -/// +///^ /// final GoRouter _router = GoRouter( /// initialLocation: '/a', /// routes: [ -/// StatefulShellRoute.rootRoutes( +/// StatefulShellRoute( /// builder: (BuildContext context, GoRouterState state, -/// Widget navigationContainer) { -/// return ScaffoldWithNavBar(body: navigationContainer); +/// Widget navigatorContainer) { +/// return ScaffoldWithNavBar(body: navigatorContainer); /// }, -/// routes: [ -/// /// The first branch, i.e. root of tab 'A' -/// GoRoute( -/// parentNavigatorKey: _tabANavigatorKey, -/// path: '/a', -/// builder: (BuildContext context, GoRouterState state) => -/// const RootScreen(label: 'A', detailsPath: '/a/details'), -/// routes: [ -/// /// Will cover screen A but not the bottom navigation bar -/// GoRoute( -/// path: 'details', -/// builder: (BuildContext context, GoRouterState state) => -/// const DetailsScreen(label: 'A'), -/// ), -/// ], +/// branches: [ +/// /// The first branch, i.e. tab 'A' +/// ShellRouteBranch( +/// navigatorKey: _tabANavigatorKey, +/// rootRoute: GoRoute( +/// path: '/a', +/// builder: (BuildContext context, GoRouterState state) => +/// const RootScreen(label: 'A', detailsPath: '/a/details'), +/// routes: [ +/// /// Will cover screen A but not the bottom navigation bar +/// GoRoute( +/// path: 'details', +/// builder: (BuildContext context, GoRouterState state) => +/// const DetailsScreen(label: 'A'), +/// ), +/// ], +/// ), /// ), -/// /// The second branch, i.e. root of tab 'B' -/// GoRoute( -/// parentNavigatorKey: _tabBNavigatorKey, -/// path: '/b', -/// builder: (BuildContext context, GoRouterState state) => -/// const RootScreen(label: 'B', detailsPath: '/b/details/1'), -/// routes: [ -/// /// Will cover screen B but not the bottom navigation bar -/// GoRoute( -/// path: 'details', -/// builder: (BuildContext context, GoRouterState state) => -/// const DetailsScreen(label: 'B'), -/// ), -/// ], +/// /// The second branch, i.e. tab 'B' +/// ShellRouteBranch( +/// navigatorKey: _tabBNavigatorKey, +/// rootRoute: GoRoute( +/// path: '/b', +/// builder: (BuildContext context, GoRouterState state) => +/// const RootScreen(label: 'B', detailsPath: '/b/details'), +/// routes: [ +/// /// Will cover screen B but not the bottom navigation bar +/// GoRoute( +/// path: 'details', +/// builder: (BuildContext context, GoRouterState state) => +/// const DetailsScreen(label: 'B'), +/// ), +/// ], +/// ), /// ), /// ], /// ), @@ -561,7 +571,7 @@ class ShellRoute extends ShellRouteBase { /// ``` /// /// When the [Page] for this route needs to be customized, you need to pass a -/// function for [pageProvider]. Note that this page provider doesn't replace +/// function for [pageBuilder]. Note that this page provider doesn't replace /// the [builder] function, but instead receives the stateful shell built by /// [StatefulShellRoute] (using the builder function) as input. In other words, /// you need to specify both when customizing a page. For example: @@ -575,7 +585,7 @@ class ShellRoute extends ShellRouteBase { /// Widget navigationContainer) { /// return ScaffoldWithNavBar(body: navigationContainer); /// }, -/// pageProvider: +/// pageBuilder: /// (BuildContext context, GoRouterState state, Widget statefulShell) { /// return NoTransitionPage(child: statefulShell); /// }, @@ -617,21 +627,15 @@ class StatefulShellRoute extends ShellRouteBase { /// A separate [Navigator] will be created for each of the branches, using /// the navigator key specified in [ShellRouteBranch]. StatefulShellRoute({ - required this.branches, - required this.builder, - this.pageProvider, - }) : assert(branches.isNotEmpty), - assert(_uniqueNavigatorKeys(branches).length == branches.length, - 'Navigator keys must be unique'), - super._(routes: _rootRoutes(branches)) { - for (int i = 0; i < routes.length; ++i) { - final RouteBase route = routes[i]; - if (route is GoRoute) { - assert(route.parentNavigatorKey == null || - route.parentNavigatorKey == branches[i].navigatorKey); - } - } - } + required List branches, + required ShellRouteBuilder builder, + ShellRoutePageBuilder? pageBuilder, + }) : this._( + routes: _rootRoutes(branches), + branches: branches, + builder: builder, + pageBuilder: pageBuilder, + ); /// Constructs a [StatefulShellRoute] from a list of [GoRoute]s, each /// representing a root in a stateful route branch. @@ -644,21 +648,40 @@ class StatefulShellRoute extends ShellRouteBase { StatefulShellRoute.rootRoutes({ required List routes, required ShellRouteBuilder builder, - ShellRoutePageBuilder? pageProvider, - }) : this( + ShellRoutePageBuilder? pageBuilder, + }) : this._( + routes: routes, branches: routes.map((GoRoute e) { - final GlobalKey? key = e.parentNavigatorKey; - assert(key != null, 'Each route must specify a parentNavigatorKey'); - return ShellRouteBranch(rootRoute: e, navigatorKey: key!); + return ShellRouteBranch( + rootRoute: e, navigatorKey: e.parentNavigatorKey); }).toList(), builder: builder, - pageProvider: pageProvider, + pageBuilder: pageBuilder, ); + StatefulShellRoute._({ + required super.routes, + required this.branches, + required this.builder, + this.pageBuilder, + }) : assert(branches.isNotEmpty), + assert(_debugUniqueNavigatorKeys(branches).length == branches.length, + 'Navigator keys must be unique'), + super._() { + for (int i = 0; i < routes.length; ++i) { + final RouteBase route = routes[i]; + if (route is GoRoute) { + assert(route.parentNavigatorKey == null || + route.parentNavigatorKey == branches[i].navigatorKey); + } + } + } + /// Representations of the different stateful route branches that this - /// shell route will manage. Each branch identifies the [Navigator] to be used - /// (via the navigatorKey) and the route that will be used as the root of the - /// route branch. + /// shell route will manage. + /// + /// Each branch identifies the [Navigator] to be used (via the navigatorKey) + /// and the route that will be used as the root of the route branch. final List branches; /// The widget builder for a stateful shell route. @@ -695,9 +718,10 @@ class StatefulShellRoute extends ShellRouteBase { /// Unlike GoRoute however, this function is used in combination with /// [builder], and the child parameter will be the stateful shell already /// built for this route, using the builder function. - final ShellRoutePageBuilder? pageProvider; + final ShellRoutePageBuilder? pageBuilder; /// The [GlobalKey]s to be used by the [Navigator]s built for this route. + /// /// StatefulShellRoute builds a Navigator for each of its route branches. /// Routes on a particular branch are then placed onto these Navigators /// instead of the root Navigator. @@ -705,8 +729,8 @@ class StatefulShellRoute extends ShellRouteBase { branches.map((ShellRouteBranch e) => e.navigatorKey).toList(); @override - GlobalKey? navigatorKeyForChildRoute(RouteBase route) { - final int routeIndex = routes.indexOf(route); + GlobalKey? navigatorKeyForSubRoute(RouteBase subRoute) { + final int routeIndex = routes.indexOf(subRoute); if (routeIndex < 0) { return null; } @@ -722,7 +746,7 @@ class StatefulShellRoute extends ShellRouteBase { return inherited!.routeState; } - static Set> _uniqueNavigatorKeys( + static Set> _debugUniqueNavigatorKeys( List branches) => Set>.from( branches.map((ShellRouteBranch e) => e.navigatorKey)); @@ -736,20 +760,28 @@ class StatefulShellRoute extends ShellRouteBase { class ShellRouteBranch { /// Constructs a [ShellRouteBranch]. ShellRouteBranch({ - required this.navigatorKey, required this.rootRoute, + GlobalKey? navigatorKey, this.defaultLocation, - }); + }) : navigatorKey = navigatorKey ?? GlobalKey(), + assert(rootRoute is GoRoute || defaultLocation != null, + 'Provide a defaultLocation or use a GoRoute as rootRoute'); - /// The [GlobalKey] to be used by the [Navigator] built for the navigation - /// branch represented by this object. All child routes will also be placed - /// onto this Navigator instead of the root Navigator. + /// The [GlobalKey] to be used by the [Navigator] built for this route branch. + /// + /// A separate Navigator will be built for each ShellRouteBranch in a + /// [StatefulShellRoute] and this key will be used to identify the Navigator. + /// The [rootRoute] and all its sub-routes will be placed o onto this Navigator + /// instead of the root Navigator. final GlobalKey navigatorKey; /// The root route of the route branch. final RouteBase rootRoute; - /// The default location for this route branch. If none is specified, the - /// location of the [rootRoute] will be used. + /// The default location for this route branch. + /// + /// If none is specified, the location of the [rootRoute] will be used. When + /// using a [rootRoute] of a different type than [GoRoute], a default location + /// must be specified. final String? defaultLocation; } From 2215a51027c2e66cd8323dc5681da9a9bdc15964 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20Lo=CC=88fstrand?= Date: Tue, 25 Oct 2022 11:57:57 +0200 Subject: [PATCH 033/112] Removed field navigatorKeys from StatefulShellRoute and fixed issue with assert (checkParentNavigatorKeys) of StatefulShellRoute in RouteConfiguration. --- packages/go_router/lib/src/configuration.dart | 16 +++++++++------- packages/go_router/lib/src/route.dart | 8 -------- 2 files changed, 9 insertions(+), 15 deletions(-) diff --git a/packages/go_router/lib/src/configuration.dart b/packages/go_router/lib/src/configuration.dart index 02e50aae4e49..c4e197c3ee21 100644 --- a/packages/go_router/lib/src/configuration.dart +++ b/packages/go_router/lib/src/configuration.dart @@ -81,13 +81,15 @@ class RouteConfiguration { ], ); } else if (route is StatefulShellRoute) { - checkParentNavigatorKeys( - route.routes, - >[ - ...allowedKeys, - ...route.navigatorKeys, - ], - ); + for (final ShellRouteBranch branch in route.branches) { + checkParentNavigatorKeys( + [branch.rootRoute], + >[ + ...allowedKeys, + branch.navigatorKey, + ], + ); + } } } } diff --git a/packages/go_router/lib/src/route.dart b/packages/go_router/lib/src/route.dart index 75fff9a3600b..7946be505c14 100644 --- a/packages/go_router/lib/src/route.dart +++ b/packages/go_router/lib/src/route.dart @@ -720,14 +720,6 @@ class StatefulShellRoute extends ShellRouteBase { /// built for this route, using the builder function. final ShellRoutePageBuilder? pageBuilder; - /// The [GlobalKey]s to be used by the [Navigator]s built for this route. - /// - /// StatefulShellRoute builds a Navigator for each of its route branches. - /// Routes on a particular branch are then placed onto these Navigators - /// instead of the root Navigator. - List> get navigatorKeys => - branches.map((ShellRouteBranch e) => e.navigatorKey).toList(); - @override GlobalKey? navigatorKeyForSubRoute(RouteBase subRoute) { final int routeIndex = routes.indexOf(subRoute); From b18065320cd24c40ad4a0a14fc9cdcfc76f2240f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20Lo=CC=88fstrand?= Date: Tue, 25 Oct 2022 12:01:43 +0200 Subject: [PATCH 034/112] Removed the use of top GoRouterState as a way of getting the current location for a branch. Removed/hid redundant fields from StatefulShellRouteState and ShellRouteBranchState. Added function goToBranch to StatefulShellRouteState. Documentation updates. --- packages/go_router/lib/src/builder.dart | 53 +++++---------- .../src/misc/stateful_navigation_shell.dart | 19 +++--- packages/go_router/lib/src/route.dart | 11 ++-- packages/go_router/lib/src/state.dart | 64 ++++++++++++------- 4 files changed, 73 insertions(+), 74 deletions(-) diff --git a/packages/go_router/lib/src/builder.dart b/packages/go_router/lib/src/builder.dart index b4af89886bd3..bc64a4f11a46 100644 --- a/packages/go_router/lib/src/builder.dart +++ b/packages/go_router/lib/src/builder.dart @@ -128,7 +128,7 @@ class RouteBuilder { } } - GoRouterState? _buildRecursive( + void _buildRecursive( BuildContext context, RouteMatchList matchList, int startIndex, @@ -139,7 +139,7 @@ class RouteBuilder { GlobalKey navigatorKey, ) { if (startIndex >= matchList.matches.length) { - return null; + return; } final RouteMatch match = matchList.matches[startIndex]; @@ -164,13 +164,11 @@ class RouteBuilder { keyToPages.putIfAbsent(goRouteNavKey, () => >[]).add(page); - return _buildRecursive(context, matchList, startIndex + 1, pop, - routerNeglect, keyToPages, newParams, navigatorKey) ?? - state; + _buildRecursive(context, matchList, startIndex + 1, pop, routerNeglect, + keyToPages, newParams, navigatorKey); } else if (route is ShellRouteBase) { - if (startIndex + 1 >= matchList.matches.length) { - throw _RouteBuilderError('Shell routes must always have child routes'); - } + assert(startIndex + 1 < matchList.matches.length, + 'Shell routes must always have child routes'); // The key for the Navigator that will display this ShellRoute's page. final GlobalKey parentNavigatorKey = navigatorKey; @@ -183,47 +181,33 @@ class RouteBuilder { // that the page for this ShellRoute is placed at the right index. final int shellPageIdx = keyToPages[parentNavigatorKey]!.length; - // Get the current child route of this shell route from the match list. - final RouteBase childRoute = matchList.matches[startIndex + 1].route; + // Get the current sub-route of this shell route from the match list. + final RouteBase subRoute = matchList.matches[startIndex + 1].route; // The key to provide to the shell route's Navigator. final GlobalKey? shellNavigatorKey = - route.navigatorKeyForSubRoute(childRoute); - if (shellNavigatorKey == null) { - throw _RouteBuilderError( - 'Shell routes must always have a navigator key'); - } + route.navigatorKeyForSubRoute(subRoute); + assert( + shellNavigatorKey != null, + 'Shell routes must always provide a navigator key for its immediate ' + 'sub-routes'); // Add an entry for the shell route's navigator - keyToPages.putIfAbsent(shellNavigatorKey, () => >[]); + keyToPages.putIfAbsent(shellNavigatorKey!, () => >[]); // Build the remaining pages and retrieve the state for the top of the // navigation stack - final GoRouterState? topRouterState = _buildRecursive( - context, - matchList, - startIndex + 1, - pop, - routerNeglect, - keyToPages, - newParams, - shellNavigatorKey, - ); + _buildRecursive(context, matchList, startIndex + 1, pop, routerNeglect, + keyToPages, newParams, shellNavigatorKey); Widget child = _buildNavigator( pop, keyToPages[shellNavigatorKey]!, shellNavigatorKey); if (route is StatefulShellRoute) { - if (topRouterState == null) { - throw _RouteBuilderError('StatefulShellRoute cannot be at the top of ' - 'the navigation stack'); - } - child = _buildStatefulNavigationShell( shellRoute: route, navigator: child as Navigator, shellRouterState: state, - topRouterState: topRouterState, ); } @@ -235,10 +219,7 @@ class RouteBuilder { keyToPages .putIfAbsent(parentNavigatorKey, () => >[]) .insert(shellPageIdx, page); - - return topRouterState; } - return null; } Navigator _buildNavigator( @@ -266,14 +247,12 @@ class RouteBuilder { required StatefulShellRoute shellRoute, required Navigator navigator, required GoRouterState shellRouterState, - required GoRouterState topRouterState, }) { return StatefulNavigationShell( configuration: configuration, shellRoute: shellRoute, activeNavigator: navigator, shellGoRouterState: shellRouterState, - topGoRouterState: topRouterState, ); } diff --git a/packages/go_router/lib/src/misc/stateful_navigation_shell.dart b/packages/go_router/lib/src/misc/stateful_navigation_shell.dart index 3f6b91a17b28..c9b963e0f0fc 100644 --- a/packages/go_router/lib/src/misc/stateful_navigation_shell.dart +++ b/packages/go_router/lib/src/misc/stateful_navigation_shell.dart @@ -6,6 +6,7 @@ import 'package:collection/collection.dart'; import 'package:flutter/widgets.dart'; import '../configuration.dart'; +import '../router.dart'; import '../typedefs.dart'; /// [InheritedWidget] for providing a reference to the closest @@ -50,7 +51,6 @@ class StatefulNavigationShell extends StatefulWidget { required this.shellRoute, required this.activeNavigator, required this.shellGoRouterState, - required this.topGoRouterState, super.key, }); @@ -66,9 +66,6 @@ class StatefulNavigationShell extends StatefulWidget { /// The [GoRouterState] for navigation shell. final GoRouterState shellGoRouterState; - /// The [GoRouterState] for the top of the current navigation stack. - final GoRouterState topGoRouterState; - @override State createState() => StatefulNavigationShellState(); } @@ -84,6 +81,10 @@ class StatefulNavigationShellState extends State { return index < 0 ? 0 : index; } + void _goToLocation(String location, Object? extra) { + GoRouter.of(context).go(location, extra: extra); + } + @override void initState() { super.initState(); @@ -94,6 +95,7 @@ class StatefulNavigationShellState extends State { )) .toList(); _routeState = StatefulShellRouteState( + goToLocation: _goToLocation, route: widget.shellRoute, branchState: branchState, index: 0, @@ -114,18 +116,17 @@ class StatefulNavigationShellState extends State { void _updateForCurrentTab() { final int currentIndex = _findCurrentIndex(); + final GoRouter goRouter = GoRouter.of(context); final List branchState = _routeState.branchState.toList(); - final ShellRouteBranchState currentBranchState = branchState[currentIndex]; - branchState[currentIndex] = ShellRouteBranchState( - routeBranch: currentBranchState.routeBranch, - rootRoutePath: currentBranchState.rootRoutePath, + branchState[currentIndex] = branchState[currentIndex].copy( navigator: widget.activeNavigator, - topGoRouterState: widget.topGoRouterState, + lastLocation: goRouter.location, ); _routeState = StatefulShellRouteState( + goToLocation: _goToLocation, route: widget.shellRoute, branchState: branchState, index: currentIndex, diff --git a/packages/go_router/lib/src/route.dart b/packages/go_router/lib/src/route.dart index 7946be505c14..ad19e2945818 100644 --- a/packages/go_router/lib/src/route.dart +++ b/packages/go_router/lib/src/route.dart @@ -340,10 +340,7 @@ abstract class ShellRouteBase extends RouteBase { const ShellRouteBase._({super.routes}) : super._(); /// Returns the key for the [Navigator] that is to be used for the specified - /// sub-route. - /// - /// The route passed as an argument to this method must be an immediate - /// sub-route to this shell route. + /// immediate sub-route of this shell route. GlobalKey? navigatorKeyForSubRoute(RouteBase subRoute); } @@ -749,6 +746,12 @@ class StatefulShellRoute extends ShellRouteBase { /// Representation of a separate branch in a stateful navigation tree, used to /// configure [StatefulShellRoute]. +/// +/// The only required argument when creating a ShellRouteBranch is the +/// [rootRoute], however in some cases you may also need to specify the +/// [defaultLocation], for instance of you're using another shell route as the +/// rootRoute. A [navigatorKey] can be useful to provide in case you need to +/// use the [Navigator] created for this branch elsewhere. class ShellRouteBranch { /// Constructs a [ShellRouteBranch]. ShellRouteBranch({ diff --git a/packages/go_router/lib/src/state.dart b/packages/go_router/lib/src/state.dart index e82dd419bf0f..68754e10ea84 100644 --- a/packages/go_router/lib/src/state.dart +++ b/packages/go_router/lib/src/state.dart @@ -6,6 +6,7 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/widgets.dart'; import 'configuration.dart'; +import 'router.dart'; /// The route state during routing. /// @@ -89,10 +90,11 @@ class GoRouterState { class StatefulShellRouteState { /// Constructs a [StatefulShellRouteState]. const StatefulShellRouteState({ + required Function(String, Object?) goToLocation, required this.route, required this.branchState, required this.index, - }); + }) : _goToLocation = goToLocation; /// The associated [StatefulShellRoute] final StatefulShellRoute route; @@ -104,10 +106,7 @@ class StatefulShellRouteState { /// The index of the currently active route branch. final int index; - /// Gets the location of the current branch. - /// - /// See [ShellRouteBranchState.location] for more details. - String get location => branchState[index].location; + final Function(String, Object?) _goToLocation; /// Gets the [Navigator]s for each of the route branches. /// @@ -116,6 +115,15 @@ class StatefulShellRouteState { List get navigators => branchState.map((ShellRouteBranchState e) => e.navigator).toList(); + /// Navigate to the current location of the branch with the provided index. + /// + /// This method will switch the currently active [Navigator] for the + /// [StatefulShellRoute] by navigating to the current location of the + /// specified branch, using the method [GoRouter.go]. + void goToBranch(int index, {Object? extra}) { + _goToLocation(branchState[index]._location, extra); + } + @override bool operator ==(Object other) { if (identical(other, this)) { @@ -140,35 +148,43 @@ class ShellRouteBranchState { /// Constructs a [ShellRouteBranchState]. const ShellRouteBranchState({ required this.routeBranch, - required this.rootRoutePath, + required String rootRoutePath, this.navigator, - this.topGoRouterState, - }); + String? lastLocation, + }) : _lastLocation = lastLocation, + _rootRoutePath = rootRoutePath; + + /// Constructs a copy of this [ShellRouteBranchState], with updated values for + /// some of the fields. + ShellRouteBranchState copy({Navigator? navigator, String? lastLocation}) { + return ShellRouteBranchState( + routeBranch: routeBranch, + rootRoutePath: _rootRoutePath, + navigator: navigator ?? this.navigator, + lastLocation: lastLocation ?? _lastLocation, + ); + } /// The associated [ShellRouteBranch] final ShellRouteBranch routeBranch; - /// The full path at which root route for the route branch is reachable. - final String rootRoutePath; - /// The [Navigator] for this route branch in a [StatefulShellRoute]. This /// field will typically not be set until this route tree has been navigated /// to at least once. final Navigator? navigator; - /// The [GoRouterState] for the top of the current navigation stack. - final GoRouterState? topGoRouterState; - /// Gets the defaultLocation specified in [routeBranch] or falls back to - /// the root path of the associated [route]. - String get defaultLocation => routeBranch.defaultLocation ?? rootRoutePath; + /// the root path of the associated [rootRoute]. + String get _defaultLocation => routeBranch.defaultLocation ?? _rootRoutePath; - /// Gets the current location from the [topGoRouterState] or falls back to - /// [defaultLocation]. - String get location => topGoRouterState?.location ?? defaultLocation; + final String? _lastLocation; + + /// The full path at which root route for the route branch is reachable. + final String _rootRoutePath; - /// The root route for the route branch. - RouteBase? get route => routeBranch.rootRoute; + /// Gets the current location for this branch or falls back to the default + /// location () if this branch hasn't been visited yet. + String get _location => _lastLocation ?? _defaultLocation; @override bool operator ==(Object other) { @@ -179,12 +195,12 @@ class ShellRouteBranchState { return false; } return other.routeBranch == routeBranch && - other.rootRoutePath == rootRoutePath && + other._rootRoutePath == _rootRoutePath && other.navigator == navigator && - other.topGoRouterState == topGoRouterState; + other._lastLocation == _lastLocation; } @override int get hashCode => - Object.hash(routeBranch, rootRoutePath, navigator, topGoRouterState); + Object.hash(routeBranch, _rootRoutePath, navigator, _lastLocation); } From 8fdfb822ea3725b1ca7cea0b77d78ca0769ef7cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20Lo=CC=88fstrand?= Date: Tue, 25 Oct 2022 12:02:57 +0200 Subject: [PATCH 035/112] Updated example to use the default constructors of StatefulShellRoute and also added a nested StatefulShellRoute with a TabBar. --- .../lib/stateful_nested_navigation.dart | 343 ++++++++++++------ 1 file changed, 236 insertions(+), 107 deletions(-) diff --git a/packages/go_router/example/lib/stateful_nested_navigation.dart b/packages/go_router/example/lib/stateful_nested_navigation.dart index 5c9990000912..157775877b47 100644 --- a/packages/go_router/example/lib/stateful_nested_navigation.dart +++ b/packages/go_router/example/lib/stateful_nested_navigation.dart @@ -2,14 +2,11 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; final GlobalKey _tabANavigatorKey = GlobalKey(debugLabel: 'tabANav'); -final GlobalKey _tabBNavigatorKey = - GlobalKey(debugLabel: 'tabBNav'); // This example demonstrates how to setup nested navigation using a // BottomNavigationBar, where each tab uses its own persistent navigator, i.e. @@ -34,58 +31,109 @@ class NestedTabNavigationExampleApp extends StatelessWidget { final GoRouter _router = GoRouter( initialLocation: '/a', + restorationScopeId: 'sadfasf', routes: [ - /// Application shell - wraps the below routes in a scaffold with - /// a bottom tab navigator (ScaffoldWithNavBar). Each tab will use its own - /// Navigator, as specified by the parentNavigatorKey for each root route - /// (branch). For more customization options for the route branches, see - /// the default constructor for StatefulShellRoute. - StatefulShellRoute.rootRoutes( - builder: (BuildContext context, GoRouterState state, - Widget navigationContainer) { - return ScaffoldWithNavBar(body: navigationContainer); - }, - routes: [ - /// The screen to display as the root in the first tab of the bottom - /// navigation bar. - GoRoute( - parentNavigatorKey: _tabANavigatorKey, - path: '/a', - builder: (BuildContext context, GoRouterState state) => - const RootScreen(label: 'A', detailsPath: '/a/details'), - routes: [ - /// The details screen to display stacked on navigator of the - /// first tab. This will cover screen A but not the application - /// shell (bottom navigation bar). - GoRoute( + StatefulShellRoute( + branches: [ + /// The route branch for the first tab of the bottom navigation bar. + ShellRouteBranch( + navigatorKey: _tabANavigatorKey, + rootRoute: GoRoute( + /// The screen to display as the root in the first tab of the bottom + /// navigation bar. + path: '/a', + builder: (BuildContext context, GoRouterState state) => + const RootScreen(label: 'A', detailsPath: '/a/details'), + routes: [ + /// The details screen to display stacked on navigator of the + /// first tab. This will cover screen A but not the application + /// shell (bottom navigation bar). + GoRoute( path: 'details', builder: (BuildContext context, GoRouterState state) => - const DetailsScreen(label: 'A')), - ], + const DetailsScreen(label: 'A'), + ), + ], + ), ), - /// The screen to display as the root in the second tab of the bottom - /// navigation bar. - GoRoute( - parentNavigatorKey: _tabBNavigatorKey, - path: '/b', - builder: (BuildContext context, GoRouterState state) => - const RootScreen( - label: 'B', - detailsPath: '/b/details/1', - detailsPath2: '/b/details/2'), - routes: [ - /// The details screen to display stacked on navigator of the - /// second tab. This will cover screen B but not the application - /// shell (bottom navigation bar). - GoRoute( - path: 'details/:param', - builder: (BuildContext context, GoRouterState state) => - DetailsScreen(label: 'B', param: state.params['param']), - ), - ], + /// The route branch for the second tab of the bottom navigation bar. + ShellRouteBranch( + /// It's not necessary to provide a navigatorKey if it isn't also + /// needed elsewhere. If not provided, a default key will be used. + // navigatorKey: _tabBNavigatorKey, + rootRoute: GoRoute( + /// The screen to display as the root in the second tab of the bottom + /// navigation bar. + path: '/b', + builder: (BuildContext context, GoRouterState state) => + const RootScreen( + label: 'B', + detailsPath: '/b/details/1', + secondDetailsPath: '/b/details/2'), + routes: [ + GoRoute( + path: 'details/:param', + builder: (BuildContext context, GoRouterState state) => + DetailsScreen(label: 'B', param: state.params['param']), + ), + ], + ), + ), + + /// The route branch for the third tab of the bottom navigation bar. + ShellRouteBranch( + /// Since this route branch has a nested StatefulShellRoute as the + /// root route, we need to specify what the default location for the + /// branch is. + defaultLocation: '/c1', + rootRoute: StatefulShellRoute.rootRoutes( + /// This bottom tab uses a nested shell, wrapping sub routes in a + /// top TabBar. In this case, we're using the `rootRoutes` + /// convenience constructor. + routes: [ + GoRoute( + path: '/c1', + builder: (BuildContext context, GoRouterState state) => + const TabScreen(label: 'C1', detailsPath: '/c1/details'), + routes: [ + GoRoute( + path: 'details', + builder: (BuildContext context, GoRouterState state) => + const DetailsScreen(label: 'C1', withScaffold: false), + ), + ], + ), + GoRoute( + path: '/c2', + builder: (BuildContext context, GoRouterState state) => + const TabScreen(label: 'C2', detailsPath: '/c2/details'), + routes: [ + GoRoute( + path: 'details', + builder: (BuildContext context, GoRouterState state) => + const DetailsScreen(label: 'C2', withScaffold: false), + ), + ], + ), + ], + builder: (BuildContext context, GoRouterState state, + Widget ignoredNavigatorContainer) { + /// For this nested StatefulShellRoute we are using a custom + /// container (TabBarView) for the branch navigators, and thus + /// ignoring the default navigator contained passed to the + /// builder. Custom implementation can access the branch + /// navigators via the StatefulShellRouteState + /// (see TabbedRootScreen for details). + return const TabbedRootScreen(); + }, + ), ), ], + builder: (BuildContext context, GoRouterState state, + Widget navigatorContainer) { + return ScaffoldWithNavBar(body: navigatorContainer); + }, /// If you need to customize the Page for StatefulShellRoute, pass a /// pageProvider function in addition to the builder, for example: @@ -93,20 +141,6 @@ class NestedTabNavigationExampleApp extends StatelessWidget { // (BuildContext context, GoRouterState state, Widget statefulShell) { // return NoTransitionPage(child: statefulShell); // }, - - /// If you need to create a custom container for the branch routes, to - /// for instance setup animations, you can implement your builder - /// something like this: - // builder: - // (BuildContext context, GoRouterState state, Widget ignoringThis) { - // final StatefulShellRouteState shellRouteState = - // StatefulShellRoute.of(context); - // return ScaffoldWithNavBar( - // body: _AnimatedRouteBranchContainer( - // currentIndex: shellRouteState.index, - // navigators: shellRouteState.navigators, - // )); - // }, ), ], ); @@ -143,20 +177,19 @@ class ScaffoldWithNavBar extends StatelessWidget { bottomNavigationBar: BottomNavigationBar( items: const [ BottomNavigationBarItem(icon: Icon(Icons.home), label: 'Section A'), - BottomNavigationBarItem( - icon: Icon(Icons.settings), label: 'Section B'), + BottomNavigationBarItem(icon: Icon(Icons.work), label: 'Section B'), + BottomNavigationBarItem(icon: Icon(Icons.tab), label: 'Section C'), ], currentIndex: shellState.index, - onTap: (int tappedIndex) => _onItemTapped( - context, - shellState.branchState[tappedIndex], - ), + onTap: (int tappedIndex) => + _onItemTapped(context, shellState, tappedIndex), ), ); } - void _onItemTapped(BuildContext context, ShellRouteBranchState routeState) { - GoRouter.of(context).go(routeState.location); + void _onItemTapped( + BuildContext context, StatefulShellRouteState shellState, int index) { + shellState.goToBranch(index); } } @@ -166,7 +199,7 @@ class RootScreen extends StatelessWidget { const RootScreen( {required this.label, required this.detailsPath, - this.detailsPath2, + this.secondDetailsPath, Key? key}) : super(key: key); @@ -177,7 +210,7 @@ class RootScreen extends StatelessWidget { final String detailsPath; /// The path to another detail page - final String? detailsPath2; + final String? secondDetailsPath; @override Widget build(BuildContext context) { @@ -199,10 +232,17 @@ class RootScreen extends StatelessWidget { child: const Text('View details'), ), const Padding(padding: EdgeInsets.all(4)), - if (detailsPath2 != null) + TextButton( + onPressed: () { + GoRouter.of(context).push('/b/details/1'); + }, + child: const Text('Push b'), + ), + const Padding(padding: EdgeInsets.all(4)), + if (secondDetailsPath != null) TextButton( onPressed: () { - GoRouter.of(context).go(detailsPath2!); + GoRouter.of(context).go(secondDetailsPath!); }, child: const Text('View more details'), ), @@ -219,6 +259,7 @@ class DetailsScreen extends StatefulWidget { const DetailsScreen({ required this.label, this.param, + this.withScaffold = true, Key? key, }) : super(key: key); @@ -228,6 +269,9 @@ class DetailsScreen extends StatefulWidget { /// Optional param final String? param; + /// Wrap in scaffold + final bool withScaffold; + @override State createState() => DetailsScreenState(); } @@ -238,54 +282,139 @@ class DetailsScreenState extends State { @override Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar( - title: Text('Details Screen - ${widget.label}'), - ), - body: Center( - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - if (widget.param != null) - Text('Parameter: ${widget.param!}', - style: Theme.of(context).textTheme.titleLarge), - const Padding(padding: EdgeInsets.all(4)), - Text('Details for ${widget.label} - Counter: $_counter', + if (widget.withScaffold) { + return Scaffold( + appBar: AppBar( + title: Text('Details Screen - ${widget.label}'), + ), + body: _build(context), + ); + } else { + return _build(context); + } + } + + Widget _build(BuildContext context) { + return Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + if (widget.param != null) + Text('Parameter: ${widget.param!}', style: Theme.of(context).textTheme.titleLarge), - const Padding(padding: EdgeInsets.all(4)), + const Padding(padding: EdgeInsets.all(4)), + Text('Details for ${widget.label} - Counter: $_counter', + style: Theme.of(context).textTheme.titleLarge), + const Padding(padding: EdgeInsets.all(4)), + TextButton( + onPressed: () { + setState(() { + _counter++; + }); + }, + child: const Text('Increment counter'), + ), + if (!widget.withScaffold) ...[ + const Padding(padding: EdgeInsets.all(16)), TextButton( onPressed: () { - setState(() { - _counter++; - }); + GoRouter.of(context).pop(); }, - child: const Text('Increment counter'), + child: const Text('< Back', + style: TextStyle(fontWeight: FontWeight.bold, fontSize: 18)), ), - ], + ] + ], + ), + ); + } +} + +/// Builds a nested shell using a [TabBar] and [TabBarView]. +class TabbedRootScreen extends StatelessWidget { + /// Constructs a TabbedRootScreen + const TabbedRootScreen({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + final StatefulShellRouteState shellState = StatefulShellRoute.of(context); + final int branchCount = shellState.branchState.length; + final List children = List.generate( + branchCount, (int index) => TabbedRootScreenTab(index: index)); + + return DefaultTabController( + length: 2, + initialIndex: shellState.index, + child: Scaffold( + appBar: AppBar( + title: const Text('Tab root'), + bottom: TabBar( + tabs: List.generate( + branchCount, (int i) => Tab(text: 'Tab $i')), + onTap: (int tappedIndex) => + _onTabTap(context, shellState, tappedIndex), + )), + body: TabBarView( + children: children, ), ), ); } + + void _onTabTap( + BuildContext context, StatefulShellRouteState shellState, int index) { + shellState.goToBranch(index); + } +} + +/// Widget wrapping the [Navigator] for a specific tab in [TabbedRootScreen]. +/// +/// This class is needed since [TabBarView] won't update its cached list of +/// children while in a transition between tabs. +class TabbedRootScreenTab extends StatelessWidget { + /// Constructs a TabbedRootScreenTab + const TabbedRootScreenTab({Key? key, required this.index}) : super(key: key); + + /// The index of the tab + final int index; + + @override + Widget build(BuildContext context) { + final StatefulShellRouteState shellState = StatefulShellRoute.of(context); + final Widget? navigator = shellState.branchState[index].navigator; + return navigator ?? const SizedBox.expand(); + } } -// ignore: unused_element -class _AnimatedRouteBranchContainer extends StatelessWidget { - const _AnimatedRouteBranchContainer( - {Key? key, required this.currentIndex, required this.navigators}) +/// Widget for the pages in the top tab bar. +class TabScreen extends StatelessWidget { + /// Creates a RootScreen + const TabScreen({required this.label, this.detailsPath, Key? key}) : super(key: key); - final int currentIndex; - final List navigators; + /// The label + final String label; + + /// The path to the detail page + final String? detailsPath; @override Widget build(BuildContext context) { - return Stack( - children: navigators.mapIndexed((int index, Widget? navigator) { - return AnimatedOpacity( - opacity: index == currentIndex ? 1 : 0, - duration: const Duration(milliseconds: 400), - child: navigator ?? const SizedBox.shrink(), - ); - }).toList()); + return Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text('Screen $label', style: Theme.of(context).textTheme.titleLarge), + const Padding(padding: EdgeInsets.all(4)), + if (detailsPath != null) + TextButton( + onPressed: () { + GoRouter.of(context).go(detailsPath!); + }, + child: const Text('View details'), + ), + ], + ), + ); } } From 9240ea44ad83468852faea5e49b9205439e9438e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20Lo=CC=88fstrand?= Date: Tue, 25 Oct 2022 23:43:34 +0200 Subject: [PATCH 036/112] Added check when pushing a new route to ensure you cannot push a route that is not a descendant of the current StatefulShellRoute branch. Minor corrections from PR feedback. Fixed broken tests. --- packages/go_router/lib/src/configuration.dart | 36 +++++++- packages/go_router/lib/src/delegate.dart | 17 +++- packages/go_router/test/builder_test.dart | 65 ++++---------- .../go_router/test/configuration_test.dart | 35 ++++++-- packages/go_router/test/delegate_test.dart | 87 +++++++++++++++++++ 5 files changed, 180 insertions(+), 60 deletions(-) diff --git a/packages/go_router/lib/src/configuration.dart b/packages/go_router/lib/src/configuration.dart index c4e197c3ee21..fae444d8c527 100644 --- a/packages/go_router/lib/src/configuration.dart +++ b/packages/go_router/lib/src/configuration.dart @@ -83,7 +83,7 @@ class RouteConfiguration { } else if (route is StatefulShellRoute) { for (final ShellRouteBranch branch in route.branches) { checkParentNavigatorKeys( - [branch.rootRoute], + [branch.rootRoute], >[ ...allowedKeys, branch.navigatorKey, @@ -181,6 +181,40 @@ class RouteConfiguration { return null; } + /// Finds the root route of closest ShellRouteBranch ancestor of the provided + /// route. + RouteBase? findAncestorShellRouteBranchRoute( + RouteBase route) { + final List ancestors = _ancestorsForRoute(route, routes); + final int shellRouteIndex = + ancestors.lastIndexWhere((RouteBase e) => e is StatefulShellRoute); + if (shellRouteIndex >= 0 && shellRouteIndex < (ancestors.length - 1)) { + return ancestors[shellRouteIndex + 1]; + } + return null; + } + + /// Tests if a route is a descendant of an ancestor route. + bool isDescendantOf({required RouteBase ancestor, required RouteBase route}) { + return _ancestorsForRoute(route, routes).contains(ancestor); + } + + static List _ancestorsForRoute( + RouteBase targetRoute, List routes) { + for (final RouteBase route in routes) { + if (route.routes.contains(targetRoute)) { + return [route]; + } else { + final List ancestors = + _ancestorsForRoute(targetRoute, route.routes); + if (ancestors.isNotEmpty) { + return [route, ...ancestors]; + } + } + } + return []; + } + @override String toString() { return 'RouterConfiguration: $routes'; diff --git a/packages/go_router/lib/src/delegate.dart b/packages/go_router/lib/src/delegate.dart index 9b6329743c29..34391b9a3267 100644 --- a/packages/go_router/lib/src/delegate.dart +++ b/packages/go_router/lib/src/delegate.dart @@ -54,7 +54,7 @@ class GoRouterDelegate extends RouterDelegate // a non-null parentNavigatorKey or a ShellRoute with a non-null // parentNavigatorKey and pop from that Navigator instead of the root. final int matchCount = _matchList.matches.length; - RouteBase? childRoute; + late RouteBase subRoute; for (int i = matchCount - 1; i >= 0; i -= 1) { final RouteMatch match = _matchList.matches[i]; final RouteBase route = match.route; @@ -67,11 +67,11 @@ class GoRouterDelegate extends RouterDelegate if (didPop) { return didPop; } - } else if (route is ShellRouteBase && childRoute != null) { + } else if (route is ShellRouteBase) { // For shell routes, find the navigator key that should be used for the // child route in the current match list final GlobalKey? navigatorKey = - route.navigatorKeyForSubRoute(childRoute); + route.navigatorKeyForSubRoute(subRoute); final bool didPop = await navigatorKey?.currentState!.maybePop() ?? false; @@ -81,7 +81,7 @@ class GoRouterDelegate extends RouterDelegate return didPop; } } - childRoute = route; + subRoute = route; } // Use the root navigator if no ShellRoute Navigators were found and didn't @@ -100,6 +100,15 @@ class GoRouterDelegate extends RouterDelegate if (match.route is ShellRouteBase) { throw GoError('ShellRoutes cannot be pushed'); } + final RouteBase? ancestorBranchRootRoute = + _configuration.findAncestorShellRouteBranchRoute(_matchList.last.route); + if (ancestorBranchRootRoute != null) { + if (!_configuration.isDescendantOf( + ancestor: ancestorBranchRootRoute, route: match.route)) { + throw GoError('Cannot push a route that is not a descendant of the ' + 'current StatefulShellRoute branch'); + } + } // Remap the pageKey to allow any number of the same page on the stack final String fullPath = match.fullpath; diff --git a/packages/go_router/test/builder_test.dart b/packages/go_router/test/builder_test.dart index ba553f5ba061..8bffb0b881ec 100644 --- a/packages/go_router/test/builder_test.dart +++ b/packages/go_router/test/builder_test.dart @@ -8,6 +8,7 @@ import 'package:go_router/src/builder.dart'; import 'package:go_router/src/configuration.dart'; import 'package:go_router/src/match.dart'; import 'package:go_router/src/matching.dart'; +import 'package:go_router/src/router.dart'; void main() { group('RouteBuilder', () { @@ -111,58 +112,30 @@ void main() { testWidgets('Builds StatefulShellRoute', (WidgetTester tester) async { final GlobalKey key = GlobalKey(debugLabel: 'key'); - final RouteConfiguration config = RouteConfiguration( + final GoRouter goRouter = GoRouter( + initialLocation: '/nested', routes: [ StatefulShellRoute.rootRoutes( - builder: - (BuildContext context, GoRouterState state, Widget child) => - child, - routes: [ - GoRoute( - parentNavigatorKey: key, - path: '/nested', - builder: (BuildContext context, GoRouterState state) { - return _DetailsScreen(); - }, - ), - ]), + builder: + (BuildContext context, GoRouterState state, Widget child) => + child, + routes: [ + GoRoute( + parentNavigatorKey: key, + path: '/nested', + builder: (BuildContext context, GoRouterState state) { + return _DetailsScreen(); + }, + ), + ], + ), ], - redirectLimit: 10, - topRedirect: (BuildContext context, GoRouterState state) { - return null; - }, navigatorKey: GlobalKey(), ); - final RouteMatchList matches = RouteMatchList([ - RouteMatch( - route: config.routes.first, - subloc: '', - fullpath: '', - encodedParams: {}, - queryParams: {}, - queryParametersAll: >{}, - extra: null, - error: null, - ), - RouteMatch( - route: config.routes.first.routes.first, - subloc: '/nested', - fullpath: '/nested', - encodedParams: {}, - queryParams: {}, - queryParametersAll: >{}, - extra: null, - error: null, - ), - ]); - - await tester.pumpWidget( - _BuilderTestWidget( - routeConfiguration: config, - matches: matches, - ), - ); + await tester.pumpWidget(MaterialApp.router( + routerConfig: goRouter, + )); expect(find.byType(_DetailsScreen), findsOneWidget); expect(find.byKey(key), findsOneWidget); diff --git a/packages/go_router/test/configuration_test.dart b/packages/go_router/test/configuration_test.dart index 925af78c5aa4..fec42ead1a46 100644 --- a/packages/go_router/test/configuration_test.dart +++ b/packages/go_router/test/configuration_test.dart @@ -81,24 +81,41 @@ void main() { }); test( - 'throws when StatefulShellRoute is missing a navigator key for a ' - 'child route', () { + 'throws when StatefulShellRoute sub-route uses incorrect parentNavigatorKey', + () { final GlobalKey root = GlobalKey(debugLabel: 'root'); final GlobalKey keyA = GlobalKey(debugLabel: 'A'); - final List shellRouteChildren = [ - GoRoute( - path: '/a', builder: _mockScreenBuilder, parentNavigatorKey: keyA), - GoRoute(path: '/b', builder: _mockScreenBuilder), - ]; + final GlobalKey keyB = + GlobalKey(debugLabel: 'B'); + expect( () { RouteConfiguration( navigatorKey: root, routes: [ - StatefulShellRoute.rootRoutes( - routes: shellRouteChildren, builder: _mockShellBuilder), + StatefulShellRoute(branches: [ + ShellRouteBranch( + navigatorKey: keyA, + rootRoute: GoRoute( + path: '/a', + builder: _mockScreenBuilder, + routes: [ + GoRoute( + path: 'details', + builder: _mockScreenBuilder, + parentNavigatorKey: keyB), + ]), + ), + ShellRouteBranch( + navigatorKey: keyB, + rootRoute: GoRoute( + path: '/b', + builder: _mockScreenBuilder, + parentNavigatorKey: keyB), + ), + ], builder: _mockShellBuilder), ], redirectLimit: 10, topRedirect: (BuildContext context, GoRouterState state) { diff --git a/packages/go_router/test/delegate_test.dart b/packages/go_router/test/delegate_test.dart index f60ce65fd95e..e91f640f6248 100644 --- a/packages/go_router/test/delegate_test.dart +++ b/packages/go_router/test/delegate_test.dart @@ -7,6 +7,7 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:go_router/go_router.dart'; import 'package:go_router/src/match.dart'; import 'package:go_router/src/misc/error_screen.dart'; +import 'package:go_router/src/misc/errors.dart'; Future createGoRouter( WidgetTester tester, { @@ -30,6 +31,39 @@ Future createGoRouter( return router; } +Future createGoRouterWithStatefulShellRoute( + WidgetTester tester) async { + final GoRouter router = GoRouter( + initialLocation: '/', + routes: [ + GoRoute(path: '/', builder: (_, __) => const DummyStatefulWidget()), + GoRoute(path: '/a', builder: (_, __) => const DummyStatefulWidget()), + StatefulShellRoute.rootRoutes(routes: [ + GoRoute( + path: '/c', + builder: (_, __) => const DummyStatefulWidget(), + routes: [ + GoRoute( + path: 'c1', builder: (_, __) => const DummyStatefulWidget()), + GoRoute( + path: 'c2', builder: (_, __) => const DummyStatefulWidget()), + ]), + GoRoute( + path: '/d', + builder: (_, __) => const DummyStatefulWidget(), + routes: [ + GoRoute( + path: 'd1', builder: (_, __) => const DummyStatefulWidget()), + ]), + ], builder: (_, __, Widget child) => child), + ], + ); + await tester.pumpWidget(MaterialApp.router( + routerConfig: router, + )); + return router; +} + void main() { group('pop', () { testWidgets('removes the last element', (WidgetTester tester) async { @@ -86,6 +120,59 @@ void main() { ); }, ); + + testWidgets( + 'It should throw GoError if pushing a route that is descendant of a ' + 'different StatefulShellRoute branch', + (WidgetTester tester) async { + final GoRouter goRouter = + await createGoRouterWithStatefulShellRoute(tester); + goRouter.push('/c/c1'); + await tester.pumpAndSettle(); + + expect( + () => goRouter.push('/d/d1'), + throwsA(isA()), + ); + await tester.pumpAndSettle(); + }, + ); + + testWidgets( + 'It should throw GoError if pushing a route that is not descendant of ' + 'the current StatefulShellRoute', + (WidgetTester tester) async { + final GoRouter goRouter = + await createGoRouterWithStatefulShellRoute(tester); + goRouter.push('/c/c1'); + await tester.pumpAndSettle(); + + expect( + () => goRouter.push('/a'), + throwsA(isA()), + ); + await tester.pumpAndSettle(); + }, + ); + + testWidgets( + 'It should successfully push a route that is a descendant of the current ' + 'StatefulShellRoute branch', + (WidgetTester tester) async { + final GoRouter goRouter = await createGoRouter(tester); + goRouter.push('/c/c1'); + await tester.pumpAndSettle(); + + goRouter.push('/c/c2'); + await tester.pumpAndSettle(); + + expect(goRouter.routerDelegate.matches.matches.length, 3); + expect( + goRouter.routerDelegate.matches.matches[2].pageKey, + const Key('/c/c2-p1'), + ); + }, + ); }); group('canPop', () { From 93bce8e64680d5e33d45cf9fa4e02a5c6ba3ac4c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20Lo=CC=88fstrand?= Date: Tue, 25 Oct 2022 23:46:20 +0200 Subject: [PATCH 037/112] Minor renaming. --- .../example/lib/stateful_nested_navigation.dart | 4 ++-- .../lib/src/misc/stateful_navigation_shell.dart | 12 ++++++------ packages/go_router/lib/src/state.dart | 10 +++++----- 3 files changed, 13 insertions(+), 13 deletions(-) diff --git a/packages/go_router/example/lib/stateful_nested_navigation.dart b/packages/go_router/example/lib/stateful_nested_navigation.dart index 157775877b47..f2917bbe7624 100644 --- a/packages/go_router/example/lib/stateful_nested_navigation.dart +++ b/packages/go_router/example/lib/stateful_nested_navigation.dart @@ -189,7 +189,7 @@ class ScaffoldWithNavBar extends StatelessWidget { void _onItemTapped( BuildContext context, StatefulShellRouteState shellState, int index) { - shellState.goToBranch(index); + shellState.goBranch(index); } } @@ -363,7 +363,7 @@ class TabbedRootScreen extends StatelessWidget { void _onTabTap( BuildContext context, StatefulShellRouteState shellState, int index) { - shellState.goToBranch(index); + shellState.goBranch(index); } } diff --git a/packages/go_router/lib/src/misc/stateful_navigation_shell.dart b/packages/go_router/lib/src/misc/stateful_navigation_shell.dart index c9b963e0f0fc..b024efecca9e 100644 --- a/packages/go_router/lib/src/misc/stateful_navigation_shell.dart +++ b/packages/go_router/lib/src/misc/stateful_navigation_shell.dart @@ -81,7 +81,7 @@ class StatefulNavigationShellState extends State { return index < 0 ? 0 : index; } - void _goToLocation(String location, Object? extra) { + void _go(String location, Object? extra) { GoRouter.of(context).go(location, extra: extra); } @@ -95,7 +95,7 @@ class StatefulNavigationShellState extends State { )) .toList(); _routeState = StatefulShellRouteState( - goToLocation: _goToLocation, + go: _go, route: widget.shellRoute, branchState: branchState, index: 0, @@ -105,16 +105,16 @@ class StatefulNavigationShellState extends State { @override void didUpdateWidget(covariant StatefulNavigationShell oldWidget) { super.didUpdateWidget(oldWidget); - _updateForCurrentTab(); + _updateRouteState(); } @override void didChangeDependencies() { super.didChangeDependencies(); - _updateForCurrentTab(); + _updateRouteState(); } - void _updateForCurrentTab() { + void _updateRouteState() { final int currentIndex = _findCurrentIndex(); final GoRouter goRouter = GoRouter.of(context); @@ -126,7 +126,7 @@ class StatefulNavigationShellState extends State { ); _routeState = StatefulShellRouteState( - goToLocation: _goToLocation, + go: _go, route: widget.shellRoute, branchState: branchState, index: currentIndex, diff --git a/packages/go_router/lib/src/state.dart b/packages/go_router/lib/src/state.dart index 68754e10ea84..5a398ccd4291 100644 --- a/packages/go_router/lib/src/state.dart +++ b/packages/go_router/lib/src/state.dart @@ -90,11 +90,11 @@ class GoRouterState { class StatefulShellRouteState { /// Constructs a [StatefulShellRouteState]. const StatefulShellRouteState({ - required Function(String, Object?) goToLocation, + required Function(String, Object?) go, required this.route, required this.branchState, required this.index, - }) : _goToLocation = goToLocation; + }) : _go = go; /// The associated [StatefulShellRoute] final StatefulShellRoute route; @@ -106,7 +106,7 @@ class StatefulShellRouteState { /// The index of the currently active route branch. final int index; - final Function(String, Object?) _goToLocation; + final Function(String, Object?) _go; /// Gets the [Navigator]s for each of the route branches. /// @@ -120,8 +120,8 @@ class StatefulShellRouteState { /// This method will switch the currently active [Navigator] for the /// [StatefulShellRoute] by navigating to the current location of the /// specified branch, using the method [GoRouter.go]. - void goToBranch(int index, {Object? extra}) { - _goToLocation(branchState[index]._location, extra); + void goBranch(int index, {Object? extra}) { + _go(branchState[index]._location, extra); } @override From 20dc0c6c810716099776b52b1a80b2422d7d6aa7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20Lo=CC=88fstrand?= Date: Wed, 26 Oct 2022 01:33:05 +0200 Subject: [PATCH 038/112] Added restorationScopeId to ShellRouteBranch and ShellRoute. --- packages/go_router/lib/src/builder.dart | 21 ++++- packages/go_router/lib/src/delegate.dart | 8 +- packages/go_router/lib/src/route.dart | 10 +++ packages/go_router/test/builder_test.dart | 104 ++++++++++++++++++++++ 4 files changed, 135 insertions(+), 8 deletions(-) diff --git a/packages/go_router/lib/src/builder.dart b/packages/go_router/lib/src/builder.dart index bc64a4f11a46..fee52ad20b66 100644 --- a/packages/go_router/lib/src/builder.dart +++ b/packages/go_router/lib/src/builder.dart @@ -2,6 +2,7 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'package:collection/collection.dart'; import 'package:flutter/widgets.dart'; import 'configuration.dart'; @@ -200,15 +201,26 @@ class RouteBuilder { _buildRecursive(context, matchList, startIndex + 1, pop, routerNeglect, keyToPages, newParams, shellNavigatorKey); - Widget child = _buildNavigator( - pop, keyToPages[shellNavigatorKey]!, shellNavigatorKey); - + Widget child; if (route is StatefulShellRoute) { + final String? restorationScopeId = route.branches + .firstWhereOrNull( + (ShellRouteBranch e) => e.navigatorKey == shellNavigatorKey) + ?.restorationScopeId; + child = _buildNavigator( + pop, keyToPages[shellNavigatorKey]!, shellNavigatorKey, + restorationScopeId: restorationScopeId); child = _buildStatefulNavigationShell( shellRoute: route, navigator: child as Navigator, shellRouterState: state, ); + } else { + final String? restorationScopeId = + (route is ShellRoute) ? route.restorationScopeId : null; + child = _buildNavigator( + pop, keyToPages[shellNavigatorKey]!, shellNavigatorKey, + restorationScopeId: restorationScopeId); } // Build the Page for this route @@ -227,10 +239,11 @@ class RouteBuilder { List> pages, Key? navigatorKey, { List observers = const [], + String? restorationScopeId, }) { return Navigator( key: navigatorKey, - restorationScopeId: restorationScopeId, + restorationScopeId: restorationScopeId ?? this.restorationScopeId, pages: pages, observers: observers, onPopPage: (Route route, dynamic result) { diff --git a/packages/go_router/lib/src/delegate.dart b/packages/go_router/lib/src/delegate.dart index 34391b9a3267..1856b80c2e30 100644 --- a/packages/go_router/lib/src/delegate.dart +++ b/packages/go_router/lib/src/delegate.dart @@ -135,7 +135,7 @@ class GoRouterDelegate extends RouterDelegate bool canPop() { // Loop through navigators in reverse and call canPop() final int matchCount = _matchList.matches.length; - RouteBase? childRoute; + late RouteBase subRoute; for (int i = matchCount - 1; i >= 0; i -= 1) { final RouteMatch match = _matchList.matches[i]; final RouteBase route = match.route; @@ -146,11 +146,11 @@ class GoRouterDelegate extends RouterDelegate if (canPop) { return canPop; } - } else if (route is ShellRouteBase && childRoute != null) { + } else if (route is ShellRouteBase) { // For shell routes, find the navigator key that should be used for the // child route in the current match list final GlobalKey? navigatorKey = - route.navigatorKeyForSubRoute(childRoute); + route.navigatorKeyForSubRoute(subRoute); final bool canPop = navigatorKey?.currentState!.canPop() ?? false; @@ -159,7 +159,7 @@ class GoRouterDelegate extends RouterDelegate return canPop; } } - childRoute = route; + subRoute = route; } return navigatorKey.currentState?.canPop() ?? false; } diff --git a/packages/go_router/lib/src/route.dart b/packages/go_router/lib/src/route.dart index ad19e2945818..f2d6208863a9 100644 --- a/packages/go_router/lib/src/route.dart +++ b/packages/go_router/lib/src/route.dart @@ -446,6 +446,7 @@ class ShellRoute extends ShellRouteBase { this.pageBuilder, super.routes, GlobalKey? navigatorKey, + this.restorationScopeId, }) : assert(routes.isNotEmpty), navigatorKey = navigatorKey ?? GlobalKey(), super._() { @@ -476,6 +477,10 @@ class ShellRoute extends ShellRouteBase { /// are placed onto this Navigator instead of the root Navigator. final GlobalKey navigatorKey; + /// Restoration ID to save and restore the state of the navigator, including + /// its history. + final String? restorationScopeId; + @override GlobalKey? navigatorKeyForSubRoute(RouteBase subRoute) { if (routes.contains(subRoute)) { @@ -758,6 +763,7 @@ class ShellRouteBranch { required this.rootRoute, GlobalKey? navigatorKey, this.defaultLocation, + this.restorationScopeId, }) : navigatorKey = navigatorKey ?? GlobalKey(), assert(rootRoute is GoRoute || defaultLocation != null, 'Provide a defaultLocation or use a GoRoute as rootRoute'); @@ -779,4 +785,8 @@ class ShellRouteBranch { /// using a [rootRoute] of a different type than [GoRoute], a default location /// must be specified. final String? defaultLocation; + + /// Restoration ID to save and restore the state of the navigator, including + /// its history. + final String? restorationScopeId; } diff --git a/packages/go_router/test/builder_test.dart b/packages/go_router/test/builder_test.dart index 8bffb0b881ec..55a6973e3e0e 100644 --- a/packages/go_router/test/builder_test.dart +++ b/packages/go_router/test/builder_test.dart @@ -318,6 +318,97 @@ void main() { expect(find.byType(_HomeScreen), findsNothing); expect(find.byType(_DetailsScreen), findsOneWidget); }); + + testWidgets('Uses the correct restorationScopeId for ShellRoute', + (WidgetTester tester) async { + final GlobalKey rootNavigatorKey = + GlobalKey(debugLabel: 'root'); + final GlobalKey shellNavigatorKey = + GlobalKey(debugLabel: 'shell'); + final RouteConfiguration config = RouteConfiguration( + navigatorKey: rootNavigatorKey, + routes: [ + ShellRoute( + builder: (BuildContext context, GoRouterState state, Widget child) { + return _HomeScreen(child: child); + }, + navigatorKey: shellNavigatorKey, + restorationScopeId: 'scope1', + routes: [ + GoRoute( + path: '/a', + builder: (BuildContext context, GoRouterState state) { + return _DetailsScreen(); + }, + ), + ], + ), + ], + redirectLimit: 10, + topRedirect: (BuildContext context, GoRouterState state) { + return null; + }, + ); + + final RouteMatchList matches = RouteMatchList([ + _createRouteMatch(config.routes.first, ''), + _createRouteMatch(config.routes.first.routes.first, '/a'), + ]); + + await tester.pumpWidget( + _BuilderTestWidget( + routeConfiguration: config, + matches: matches, + ), + ); + + expect(find.byKey(rootNavigatorKey), findsOneWidget); + expect(find.byKey(shellNavigatorKey), findsOneWidget); + expect( + (shellNavigatorKey.currentWidget as Navigator?)?.restorationScopeId, + 'scope1'); + }); + + testWidgets('Uses the correct restorationScopeId for StatefulShellRoute', + (WidgetTester tester) async { + final GlobalKey rootNavigatorKey = + GlobalKey(debugLabel: 'root'); + final GlobalKey shellNavigatorKey = + GlobalKey(debugLabel: 'shell'); + final GoRouter goRouter = GoRouter( + initialLocation: '/a', + navigatorKey: rootNavigatorKey, + routes: [ + StatefulShellRoute( + builder: (BuildContext context, GoRouterState state, Widget child) { + return _HomeScreen(child: child); + }, + branches: [ + ShellRouteBranch( + navigatorKey: shellNavigatorKey, + restorationScopeId: 'scope1', + rootRoute: GoRoute( + path: '/a', + builder: (BuildContext context, GoRouterState state) { + return _DetailsScreen(); + }, + ), + ), + ], + ), + ], + ); + + await tester.pumpWidget(MaterialApp.router( + routerConfig: goRouter, + )); + + expect(find.byKey(rootNavigatorKey), findsOneWidget); + expect(find.byKey(shellNavigatorKey), findsOneWidget); + expect( + (shellNavigatorKey.currentWidget as Navigator?)?.restorationScopeId, + 'scope1'); + }); }); } @@ -399,3 +490,16 @@ class _BuilderTestWidget extends StatelessWidget { ); } } + +RouteMatch _createRouteMatch(RouteBase route, String location) { + return RouteMatch( + route: route, + subloc: location, + fullpath: location, + encodedParams: {}, + queryParams: {}, + queryParametersAll: >{}, + extra: null, + error: null, + ); +} From 2b2ff910a50e2670fece67e8b23c5a211cf49ff0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20Lo=CC=88fstrand?= Date: Thu, 27 Oct 2022 21:56:58 +0200 Subject: [PATCH 039/112] Added support for maintaining any extra navigation object passed to the current route of a route branch. Some documentation updates. --- .../lib/stateful_nested_navigation.dart | 44 +++++++++------ packages/go_router/lib/src/builder.dart | 7 ++- .../src/misc/stateful_navigation_shell.dart | 25 +++++---- packages/go_router/lib/src/state.dart | 36 +++++++------ packages/go_router/test/builder_test.dart | 2 +- packages/go_router/test/go_router_test.dart | 54 +++++++++++++++++++ 6 files changed, 124 insertions(+), 44 deletions(-) diff --git a/packages/go_router/example/lib/stateful_nested_navigation.dart b/packages/go_router/example/lib/stateful_nested_navigation.dart index f2917bbe7624..477759845009 100644 --- a/packages/go_router/example/lib/stateful_nested_navigation.dart +++ b/packages/go_router/example/lib/stateful_nested_navigation.dart @@ -51,7 +51,7 @@ class NestedTabNavigationExampleApp extends StatelessWidget { GoRoute( path: 'details', builder: (BuildContext context, GoRouterState state) => - const DetailsScreen(label: 'A'), + DetailsScreen(label: 'A', extra: state.extra), ), ], ), @@ -75,7 +75,10 @@ class NestedTabNavigationExampleApp extends StatelessWidget { GoRoute( path: 'details/:param', builder: (BuildContext context, GoRouterState state) => - DetailsScreen(label: 'B', param: state.params['param']), + DetailsScreen( + label: 'B', + param: state.params['param'], + extra: state.extra), ), ], ), @@ -100,7 +103,10 @@ class NestedTabNavigationExampleApp extends StatelessWidget { GoRoute( path: 'details', builder: (BuildContext context, GoRouterState state) => - const DetailsScreen(label: 'C1', withScaffold: false), + DetailsScreen( + label: 'C1', + extra: state.extra, + withScaffold: false), ), ], ), @@ -112,7 +118,10 @@ class NestedTabNavigationExampleApp extends StatelessWidget { GoRoute( path: 'details', builder: (BuildContext context, GoRouterState state) => - const DetailsScreen(label: 'C2', withScaffold: false), + DetailsScreen( + label: 'C2', + extra: state.extra, + withScaffold: false), ), ], ), @@ -227,18 +236,11 @@ class RootScreen extends StatelessWidget { const Padding(padding: EdgeInsets.all(4)), TextButton( onPressed: () { - GoRouter.of(context).go(detailsPath); + GoRouter.of(context).go(detailsPath, extra: '$label-XYZ'); }, child: const Text('View details'), ), const Padding(padding: EdgeInsets.all(4)), - TextButton( - onPressed: () { - GoRouter.of(context).push('/b/details/1'); - }, - child: const Text('Push b'), - ), - const Padding(padding: EdgeInsets.all(4)), if (secondDetailsPath != null) TextButton( onPressed: () { @@ -259,6 +261,7 @@ class DetailsScreen extends StatefulWidget { const DetailsScreen({ required this.label, this.param, + this.extra, this.withScaffold = true, Key? key, }) : super(key: key); @@ -269,6 +272,9 @@ class DetailsScreen extends StatefulWidget { /// Optional param final String? param; + /// Optional extra object + final Object? extra; + /// Wrap in scaffold final bool withScaffold; @@ -299,10 +305,6 @@ class DetailsScreenState extends State { child: Column( mainAxisSize: MainAxisSize.min, children: [ - if (widget.param != null) - Text('Parameter: ${widget.param!}', - style: Theme.of(context).textTheme.titleLarge), - const Padding(padding: EdgeInsets.all(4)), Text('Details for ${widget.label} - Counter: $_counter', style: Theme.of(context).textTheme.titleLarge), const Padding(padding: EdgeInsets.all(4)), @@ -314,6 +316,14 @@ class DetailsScreenState extends State { }, child: const Text('Increment counter'), ), + const Padding(padding: EdgeInsets.all(8)), + if (widget.param != null) + Text('Parameter: ${widget.param!}', + style: Theme.of(context).textTheme.titleMedium), + const Padding(padding: EdgeInsets.all(8)), + if (widget.extra != null) + Text('Extra: ${widget.extra!}', + style: Theme.of(context).textTheme.titleMedium), if (!widget.withScaffold) ...[ const Padding(padding: EdgeInsets.all(16)), TextButton( @@ -350,7 +360,7 @@ class TabbedRootScreen extends StatelessWidget { title: const Text('Tab root'), bottom: TabBar( tabs: List.generate( - branchCount, (int i) => Tab(text: 'Tab $i')), + branchCount, (int i) => Tab(text: 'Tab ${i + 1}')), onTap: (int tappedIndex) => _onTabTap(context, shellState, tappedIndex), )), diff --git a/packages/go_router/lib/src/builder.dart b/packages/go_router/lib/src/builder.dart index fee52ad20b66..f6500dabd8ac 100644 --- a/packages/go_router/lib/src/builder.dart +++ b/packages/go_router/lib/src/builder.dart @@ -212,8 +212,9 @@ class RouteBuilder { restorationScopeId: restorationScopeId); child = _buildStatefulNavigationShell( shellRoute: route, - navigator: child as Navigator, shellRouterState: state, + navigator: child as Navigator, + matchList: matchList, ); } else { final String? restorationScopeId = @@ -260,12 +261,14 @@ class RouteBuilder { required StatefulShellRoute shellRoute, required Navigator navigator, required GoRouterState shellRouterState, + required RouteMatchList matchList, }) { return StatefulNavigationShell( configuration: configuration, shellRoute: shellRoute, - activeNavigator: navigator, shellGoRouterState: shellRouterState, + navigator: navigator, + matchList: matchList, ); } diff --git a/packages/go_router/lib/src/misc/stateful_navigation_shell.dart b/packages/go_router/lib/src/misc/stateful_navigation_shell.dart index b024efecca9e..f1ec68dea750 100644 --- a/packages/go_router/lib/src/misc/stateful_navigation_shell.dart +++ b/packages/go_router/lib/src/misc/stateful_navigation_shell.dart @@ -6,6 +6,7 @@ import 'package:collection/collection.dart'; import 'package:flutter/widgets.dart'; import '../configuration.dart'; +import '../matching.dart'; import '../router.dart'; import '../typedefs.dart'; @@ -49,8 +50,9 @@ class StatefulNavigationShell extends StatefulWidget { const StatefulNavigationShell({ required this.configuration, required this.shellRoute, - required this.activeNavigator, required this.shellGoRouterState, + required this.navigator, + required this.matchList, super.key, }); @@ -60,12 +62,15 @@ class StatefulNavigationShell extends StatefulWidget { /// The associated [StatefulShellRoute] final StatefulShellRoute shellRoute; - /// The navigator for the currently active tab - final Navigator activeNavigator; - - /// The [GoRouterState] for navigation shell. + /// The [GoRouterState] for the navigation shell. final GoRouterState shellGoRouterState; + /// The navigator for the currently active route branch + final Navigator navigator; + + /// The RouteMatchList for the current location + final RouteMatchList matchList; + @override State createState() => StatefulNavigationShellState(); } @@ -77,7 +82,7 @@ class StatefulNavigationShellState extends State { int _findCurrentIndex() { final List branchState = _routeState.branchState; final int index = branchState.indexWhere((ShellRouteBranchState e) => - e.routeBranch.navigatorKey == widget.activeNavigator.key); + e.routeBranch.navigatorKey == widget.navigator.key); return index < 0 ? 0 : index; } @@ -116,13 +121,15 @@ class StatefulNavigationShellState extends State { void _updateRouteState() { final int currentIndex = _findCurrentIndex(); - final GoRouter goRouter = GoRouter.of(context); + final RouteMatchList matchList = widget.matchList; + final String location = matchList.location.toString(); + final Object? extra = matchList.extra; final List branchState = _routeState.branchState.toList(); branchState[currentIndex] = branchState[currentIndex].copy( - navigator: widget.activeNavigator, - lastLocation: goRouter.location, + navigator: widget.navigator, + lastRouteInformation: RouteInformation(location: location, state: extra), ); _routeState = StatefulShellRouteState( diff --git a/packages/go_router/lib/src/state.dart b/packages/go_router/lib/src/state.dart index 5a398ccd4291..225e43d9dcc4 100644 --- a/packages/go_router/lib/src/state.dart +++ b/packages/go_router/lib/src/state.dart @@ -121,7 +121,8 @@ class StatefulShellRouteState { /// [StatefulShellRoute] by navigating to the current location of the /// specified branch, using the method [GoRouter.go]. void goBranch(int index, {Object? extra}) { - _go(branchState[index]._location, extra); + _go(branchState[index]._location, + extra ?? branchState[index]._lastRouteInformation?.state); } @override @@ -150,41 +151,46 @@ class ShellRouteBranchState { required this.routeBranch, required String rootRoutePath, this.navigator, - String? lastLocation, - }) : _lastLocation = lastLocation, + RouteInformation? lastRouteInformation, + }) : _lastRouteInformation = lastRouteInformation, _rootRoutePath = rootRoutePath; /// Constructs a copy of this [ShellRouteBranchState], with updated values for /// some of the fields. - ShellRouteBranchState copy({Navigator? navigator, String? lastLocation}) { + ShellRouteBranchState copy( + {Navigator? navigator, RouteInformation? lastRouteInformation}) { return ShellRouteBranchState( routeBranch: routeBranch, rootRoutePath: _rootRoutePath, navigator: navigator ?? this.navigator, - lastLocation: lastLocation ?? _lastLocation, + lastRouteInformation: lastRouteInformation ?? _lastRouteInformation, ); } /// The associated [ShellRouteBranch] final ShellRouteBranch routeBranch; - /// The [Navigator] for this route branch in a [StatefulShellRoute]. This - /// field will typically not be set until this route tree has been navigated + /// The [Navigator] for this route branch in a [StatefulShellRoute]. + /// + /// This field will typically not be set until this route tree has been navigated /// to at least once. final Navigator? navigator; /// Gets the defaultLocation specified in [routeBranch] or falls back to - /// the root path of the associated [rootRoute]. + /// the path of the root route of the branch. String get _defaultLocation => routeBranch.defaultLocation ?? _rootRoutePath; - final String? _lastLocation; + final RouteInformation? _lastRouteInformation; /// The full path at which root route for the route branch is reachable. final String _rootRoutePath; - /// Gets the current location for this branch or falls back to the default - /// location () if this branch hasn't been visited yet. - String get _location => _lastLocation ?? _defaultLocation; + /// Gets the current location for this branch. + /// + /// Returns the last location navigated to on this route branch. If this + /// branch hasn't been visited yet, the default location will be used + /// (see [ShellRouteBranch.defaultLocation]). + String get _location => _lastRouteInformation?.location ?? _defaultLocation; @override bool operator ==(Object other) { @@ -197,10 +203,10 @@ class ShellRouteBranchState { return other.routeBranch == routeBranch && other._rootRoutePath == _rootRoutePath && other.navigator == navigator && - other._lastLocation == _lastLocation; + other._lastRouteInformation == _lastRouteInformation; } @override - int get hashCode => - Object.hash(routeBranch, _rootRoutePath, navigator, _lastLocation); + int get hashCode => Object.hash( + routeBranch, _rootRoutePath, navigator, _lastRouteInformation); } diff --git a/packages/go_router/test/builder_test.dart b/packages/go_router/test/builder_test.dart index 55a6973e3e0e..a6f4b77ab300 100644 --- a/packages/go_router/test/builder_test.dart +++ b/packages/go_router/test/builder_test.dart @@ -383,7 +383,7 @@ void main() { builder: (BuildContext context, GoRouterState state, Widget child) { return _HomeScreen(child: child); }, - branches: [ + branches: [ ShellRouteBranch( navigatorKey: shellNavigatorKey, restorationScopeId: 'scope1', diff --git a/packages/go_router/test/go_router_test.dart b/packages/go_router/test/go_router_test.dart index c102ffb37d8d..9536fcf5db86 100644 --- a/packages/go_router/test/go_router_test.dart +++ b/packages/go_router/test/go_router_test.dart @@ -2541,6 +2541,60 @@ void main() { expect(find.text('Screen B'), findsOneWidget); expect(find.text('Screen B Detail'), findsNothing); }); + + testWidgets( + 'Maintains extra navigation information when navigating ' + 'between branches in StatefulShellRoute', (WidgetTester tester) async { + final GlobalKey rootNavigatorKey = + GlobalKey(); + final GlobalKey sectionANavigatorKey = + GlobalKey(); + final GlobalKey sectionBNavigatorKey = + GlobalKey(); + StatefulShellRouteState? routeState; + + final List routes = [ + StatefulShellRoute.rootRoutes( + builder: (BuildContext context, GoRouterState state, Widget child) { + routeState = StatefulShellRoute.of(context); + return child; + }, + routes: [ + GoRoute( + parentNavigatorKey: sectionANavigatorKey, + path: '/a', + builder: (BuildContext context, GoRouterState state) => + const Text('Screen A'), + ), + GoRoute( + parentNavigatorKey: sectionBNavigatorKey, + path: '/b', + builder: (BuildContext context, GoRouterState state) => + Text('Screen B - ${state.extra}'), + ), + ], + ), + ]; + + final GoRouter router = await createRouter(routes, tester, + initialLocation: '/a', navigatorKey: rootNavigatorKey); + expect(find.text('Screen A'), findsOneWidget); + + router.go('/b', extra: 'X'); + await tester.pumpAndSettle(); + expect(find.text('Screen A'), findsNothing); + expect(find.text('Screen B - X'), findsOneWidget); + + routeState!.goBranch(0); + await tester.pumpAndSettle(); + expect(find.text('Screen A'), findsOneWidget); + expect(find.text('Screen B - X'), findsNothing); + + routeState!.goBranch(1); + await tester.pumpAndSettle(); + expect(find.text('Screen A'), findsNothing); + expect(find.text('Screen B - X'), findsOneWidget); + }); }); group('Imperative navigation', () { From 5c9fe04de8af622ddf4a1f3c9a2ced3934a5bcef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20Lo=CC=88fstrand?= Date: Fri, 28 Oct 2022 10:17:02 +0200 Subject: [PATCH 040/112] Moved NEXT info 5.2.0 in changelog --- packages/go_router/CHANGELOG.md | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/packages/go_router/CHANGELOG.md b/packages/go_router/CHANGELOG.md index a2fada040399..c62c22834288 100644 --- a/packages/go_router/CHANGELOG.md +++ b/packages/go_router/CHANGELOG.md @@ -1,8 +1,3 @@ -## NEXT - -- Update README -- Removes dynamic calls in examples. - ## 5.2.0 - Introduced a new shell route class called `StatefulShellRoute`, to support using separate @@ -10,6 +5,8 @@ (flutter/flutter#99124). - Updated documentation for `pageBuilder` and `builder` fields of `ShellRoute`, to more correctly describe the meaning of the child argument in the builder functions. +- Update README +- Removes dynamic calls in examples. ## 5.1.1 From 81e12964f04a28c042ec80a6fb84e352e3b0c9b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20Lo=CC=88fstrand?= Date: Fri, 28 Oct 2022 10:35:35 +0200 Subject: [PATCH 041/112] Added assertion in RouteConfiguration for field defaultLocation of ShellRouteBranch. Updated assertion when pushing descendants of StatefulShellRoute. --- packages/go_router/lib/src/configuration.dart | 39 ++++- packages/go_router/lib/src/delegate.dart | 2 +- .../go_router/test/configuration_test.dart | 159 ++++++++++++++++++ packages/go_router/test/delegate_test.dart | 48 +++++- 4 files changed, 243 insertions(+), 5 deletions(-) diff --git a/packages/go_router/lib/src/configuration.dart b/packages/go_router/lib/src/configuration.dart index fae444d8c527..00019bb4d7af 100644 --- a/packages/go_router/lib/src/configuration.dart +++ b/packages/go_router/lib/src/configuration.dart @@ -6,6 +6,7 @@ import 'package:flutter/widgets.dart'; import 'configuration.dart'; import 'logging.dart'; +import 'matching.dart'; import 'path_utils.dart'; import 'typedefs.dart'; @@ -96,6 +97,37 @@ class RouteConfiguration { checkParentNavigatorKeys( routes, >[navigatorKey]); + + void checkShellRouteBranchDefaultLocations( + List routes, RouteMatcher matcher) { + try { + for (final RouteBase route in routes) { + if (route is StatefulShellRoute) { + for (final ShellRouteBranch branch in route.branches) { + if (branch.defaultLocation == null) { + continue; + } + final RouteBase defaultLocationRoute = + matcher.findMatch(branch.defaultLocation!).last.route; + + assert(branch.rootRoute == defaultLocationRoute || + isDescendantOrSame( + ancestor: branch.rootRoute, + route: defaultLocationRoute)); + } + } + checkShellRouteBranchDefaultLocations(route.routes, matcher); + } + } on MatcherError catch (e) { + assert( + false, + 'defaultLocation (${e.location}) of ShellRouteBranch must ' + 'be a valid location'); + } + } + + checkShellRouteBranchDefaultLocations(routes, RouteMatcher(this)); + return true; }()); } @@ -194,8 +226,9 @@ class RouteConfiguration { return null; } - /// Tests if a route is a descendant of an ancestor route. - bool isDescendantOf({required RouteBase ancestor, required RouteBase route}) { + /// Tests if a route is a descendant of, or same as, an ancestor route. + bool isDescendantOrSame( + {required RouteBase ancestor, required RouteBase route}) { return _ancestorsForRoute(route, routes).contains(ancestor); } @@ -203,7 +236,7 @@ class RouteConfiguration { RouteBase targetRoute, List routes) { for (final RouteBase route in routes) { if (route.routes.contains(targetRoute)) { - return [route]; + return [route, targetRoute]; } else { final List ancestors = _ancestorsForRoute(targetRoute, route.routes); diff --git a/packages/go_router/lib/src/delegate.dart b/packages/go_router/lib/src/delegate.dart index 1856b80c2e30..d9af2d79e9fa 100644 --- a/packages/go_router/lib/src/delegate.dart +++ b/packages/go_router/lib/src/delegate.dart @@ -103,7 +103,7 @@ class GoRouterDelegate extends RouterDelegate final RouteBase? ancestorBranchRootRoute = _configuration.findAncestorShellRouteBranchRoute(_matchList.last.route); if (ancestorBranchRootRoute != null) { - if (!_configuration.isDescendantOf( + if (!_configuration.isDescendantOrSame( ancestor: ancestorBranchRootRoute, route: match.route)) { throw GoError('Cannot push a route that is not a descendant of the ' 'current StatefulShellRoute branch'); diff --git a/packages/go_router/test/configuration_test.dart b/packages/go_router/test/configuration_test.dart index fec42ead1a46..a6a8d76982c0 100644 --- a/packages/go_router/test/configuration_test.dart +++ b/packages/go_router/test/configuration_test.dart @@ -195,6 +195,165 @@ void main() { ); }); + test( + 'throws when a branch of a StatefulShellRoute has an incorrect ' + 'defaultLocation', () { + final GlobalKey root = + GlobalKey(debugLabel: 'root'); + final GlobalKey sectionANavigatorKey = + GlobalKey(); + final GlobalKey sectionBNavigatorKey = + GlobalKey(); + expect( + () { + RouteConfiguration( + navigatorKey: root, + routes: [ + StatefulShellRoute(branches: [ + ShellRouteBranch( + defaultLocation: '/x', + navigatorKey: sectionANavigatorKey, + rootRoute: GoRoute( + path: '/a', + builder: _mockScreenBuilder, + ), + ), + ShellRouteBranch( + navigatorKey: sectionBNavigatorKey, + rootRoute: GoRoute( + path: '/b', + builder: _mockScreenBuilder, + ), + ), + ], builder: _mockShellBuilder), + ], + redirectLimit: 10, + topRedirect: (BuildContext context, GoRouterState state) { + return null; + }, + ); + }, + throwsAssertionError, + ); + }); + + test( + 'throws when a branch of a StatefulShellRoute has a defaultLocation ' + 'that belongs to another branch', () { + final GlobalKey root = + GlobalKey(debugLabel: 'root'); + final GlobalKey sectionANavigatorKey = + GlobalKey(); + final GlobalKey sectionBNavigatorKey = + GlobalKey(); + expect( + () { + RouteConfiguration( + navigatorKey: root, + routes: [ + StatefulShellRoute(branches: [ + ShellRouteBranch( + defaultLocation: '/b', + navigatorKey: sectionANavigatorKey, + rootRoute: GoRoute( + path: '/a', + builder: _mockScreenBuilder, + ), + ), + ShellRouteBranch( + defaultLocation: '/b', + navigatorKey: sectionBNavigatorKey, + rootRoute: StatefulShellRoute(branches: [ + ShellRouteBranch( + rootRoute: GoRoute( + path: '/b', + builder: _mockScreenBuilder, + ), + ), + ], builder: _mockShellBuilder), + ), + ], builder: _mockShellBuilder), + ], + redirectLimit: 10, + topRedirect: (BuildContext context, GoRouterState state) { + return null; + }, + ); + }, + throwsAssertionError, + ); + }); + + test( + 'does not throw when a branch of a StatefulShellRoute has correctly ' + 'configured defaultLocations', () { + final GlobalKey root = + GlobalKey(debugLabel: 'root'); + + RouteConfiguration( + navigatorKey: root, + routes: [ + StatefulShellRoute(branches: [ + ShellRouteBranch( + rootRoute: GoRoute( + path: '/a', + builder: _mockScreenBuilder, + routes: [ + GoRoute( + path: 'detail', + builder: _mockScreenBuilder, + ), + ]), + ), + ShellRouteBranch( + defaultLocation: '/b/detail', + rootRoute: GoRoute( + path: '/b', + builder: _mockScreenBuilder, + routes: [ + GoRoute( + path: 'detail', + builder: _mockScreenBuilder, + ), + ]), + ), + ShellRouteBranch( + defaultLocation: '/c/detail', + rootRoute: StatefulShellRoute(branches: [ + ShellRouteBranch( + rootRoute: GoRoute( + path: '/c', + builder: _mockScreenBuilder, + routes: [ + GoRoute( + path: 'detail', + builder: _mockScreenBuilder, + ), + ]), + ), + ShellRouteBranch( + defaultLocation: '/d/detail', + rootRoute: GoRoute( + path: '/d', + builder: _mockScreenBuilder, + routes: [ + GoRoute( + path: 'detail', + builder: _mockScreenBuilder, + ), + ]), + ), + ], builder: _mockShellBuilder), + ), + ], builder: _mockShellBuilder), + ], + redirectLimit: 10, + topRedirect: (BuildContext context, GoRouterState state) { + return null; + }, + ); + }); + test( 'throws when there is a GoRoute ancestor with a different parentNavigatorKey', () { diff --git a/packages/go_router/test/delegate_test.dart b/packages/go_router/test/delegate_test.dart index e91f640f6248..588664718d5c 100644 --- a/packages/go_router/test/delegate_test.dart +++ b/packages/go_router/test/delegate_test.dart @@ -147,6 +147,31 @@ void main() { goRouter.push('/c/c1'); await tester.pumpAndSettle(); + expect( + () => goRouter.push('/a'), + throwsA(isA()), + ); + }, + ); + + testWidgets( + 'It should throw GoError if pushing a route that belongs to a different ' + 'StatefulShellRoute', + (WidgetTester tester) async { + final GoRouter goRouter = + await createGoRouterWithStatefulShellRoute(tester); + goRouter.push('/c'); + await tester.pumpAndSettle(); + + expect( + () => goRouter.push('/d'), + throwsA(isA()), + ); + await tester.pumpAndSettle(); + + goRouter.push('/c/c1'); + await tester.pumpAndSettle(); + expect( () => goRouter.push('/a'), throwsA(isA()), @@ -159,7 +184,8 @@ void main() { 'It should successfully push a route that is a descendant of the current ' 'StatefulShellRoute branch', (WidgetTester tester) async { - final GoRouter goRouter = await createGoRouter(tester); + final GoRouter goRouter = + await createGoRouterWithStatefulShellRoute(tester); goRouter.push('/c/c1'); await tester.pumpAndSettle(); @@ -173,6 +199,26 @@ void main() { ); }, ); + + testWidgets( + 'It should successfully push the root of the current StatefulShellRoute ' + 'branch upon itself', + (WidgetTester tester) async { + final GoRouter goRouter = + await createGoRouterWithStatefulShellRoute(tester); + goRouter.push('/c'); + await tester.pumpAndSettle(); + + goRouter.push('/c'); + await tester.pumpAndSettle(); + + expect(goRouter.routerDelegate.matches.matches.length, 3); + expect( + goRouter.routerDelegate.matches.matches[2].pageKey, + const Key('/c-p2'), + ); + }, + ); }); group('canPop', () { From 59e3b668fb0b399e1df47a39960a37fd7b22a693 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20Lo=CC=88fstrand?= Date: Fri, 28 Oct 2022 10:39:47 +0200 Subject: [PATCH 042/112] Added field preloadBranches to StatefulShellRoute, to enable support for preloading the root routes of the branches. --- .../lib/stateful_nested_navigation.dart | 9 ++- packages/go_router/lib/src/builder.dart | 33 ++++++++++ .../src/misc/stateful_navigation_shell.dart | 25 +++++++- packages/go_router/lib/src/route.dart | 13 ++++ packages/go_router/lib/src/state.dart | 4 +- packages/go_router/test/go_router_test.dart | 62 +++++++++++++++++++ 6 files changed, 142 insertions(+), 4 deletions(-) diff --git a/packages/go_router/example/lib/stateful_nested_navigation.dart b/packages/go_router/example/lib/stateful_nested_navigation.dart index 477759845009..4b043de0f6ea 100644 --- a/packages/go_router/example/lib/stateful_nested_navigation.dart +++ b/packages/go_router/example/lib/stateful_nested_navigation.dart @@ -31,9 +31,11 @@ class NestedTabNavigationExampleApp extends StatelessWidget { final GoRouter _router = GoRouter( initialLocation: '/a', - restorationScopeId: 'sadfasf', routes: [ StatefulShellRoute( + /// To enable preloading of the root routes of the branches, pass true + /// for the parameter preloadBranches. + // preloadBranches: true, branches: [ /// The route branch for the first tab of the bottom navigation bar. ShellRouteBranch( @@ -410,6 +412,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 default location ('/c1') + 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 f6500dabd8ac..45474d477afc 100644 --- a/packages/go_router/lib/src/builder.dart +++ b/packages/go_router/lib/src/builder.dart @@ -215,6 +215,7 @@ class RouteBuilder { shellRouterState: state, navigator: child as Navigator, matchList: matchList, + pop: pop, ); } else { final String? restorationScopeId = @@ -262,6 +263,7 @@ class RouteBuilder { required Navigator navigator, required GoRouterState shellRouterState, required RouteMatchList matchList, + required VoidCallback pop, }) { return StatefulNavigationShell( configuration: configuration, @@ -269,9 +271,40 @@ class RouteBuilder { shellGoRouterState: shellRouterState, navigator: navigator, matchList: matchList, + branchNavigatorBuilder: (BuildContext context, + StatefulShellRouteState routeState, int branchIndex) => + _buildPreloadedNavigatorForRouteBranch( + context, routeState, branchIndex, pop), ); } + Navigator? _buildPreloadedNavigatorForRouteBranch(BuildContext context, + StatefulShellRouteState routeState, int branchIndex, VoidCallback pop) { + final ShellRouteBranchState branchState = + routeState.branchState[branchIndex]; + final ShellRouteBranch branch = branchState.routeBranch; + final String defaultLocation = branchState.defaultLocation; + + // Build a RouteMatchList from the default location of the route branch and + // find the index of the branch root route in the match list + RouteMatchList routeMatchList = + RouteMatcher(configuration).findMatch(defaultLocation); + final int routeBranchIndex = routeMatchList.matches + .indexWhere((RouteMatch e) => e.route == branch.rootRoute); + + // Keep only the routes from and below the root route in the match list + if (routeBranchIndex >= 0) { + routeMatchList = + RouteMatchList(routeMatchList.matches.sublist(routeBranchIndex)); + // Build the pages and the Navigator for the route branch + final List> pages = + buildPages(context, routeMatchList, pop, true, branch.navigatorKey); + return _buildNavigator(pop, pages, branch.navigatorKey, + restorationScopeId: branch.restorationScopeId); + } + return null; + } + /// Helper method that builds a [GoRouterState] object for the given [match] /// and [params]. @visibleForTesting diff --git a/packages/go_router/lib/src/misc/stateful_navigation_shell.dart b/packages/go_router/lib/src/misc/stateful_navigation_shell.dart index f1ec68dea750..120f05cedf1c 100644 --- a/packages/go_router/lib/src/misc/stateful_navigation_shell.dart +++ b/packages/go_router/lib/src/misc/stateful_navigation_shell.dart @@ -30,6 +30,10 @@ class InheritedStatefulNavigationShell extends InheritedWidget { } } +/// Builder function for a route branch navigator +typedef ShellRouteBranchNavigatorBuilder = Navigator? Function( + BuildContext context, StatefulShellRouteState routeState, int branchIndex); + /// Widget that manages and maintains the state of a [StatefulShellRoute], /// including the [Navigator]s of the configured route branches. /// @@ -53,6 +57,7 @@ class StatefulNavigationShell extends StatefulWidget { required this.shellGoRouterState, required this.navigator, required this.matchList, + required this.branchNavigatorBuilder, super.key, }); @@ -71,6 +76,9 @@ class StatefulNavigationShell extends StatefulWidget { /// The RouteMatchList for the current location final RouteMatchList matchList; + /// Builder for route branch navigators (used for preloading). + final ShellRouteBranchNavigatorBuilder branchNavigatorBuilder; + @override State createState() => StatefulNavigationShellState(); } @@ -90,13 +98,16 @@ class StatefulNavigationShellState extends State { GoRouter.of(context).go(location, extra: extra); } + String _fullPathForRoute(RouteBase route) => + widget.configuration.fullPathForRoute(route); + @override void initState() { super.initState(); final List branchState = widget.shellRoute.branches .map((ShellRouteBranch e) => ShellRouteBranchState( routeBranch: e, - rootRoutePath: widget.configuration.fullPathForRoute(e.rootRoute), + rootRoutePath: _fullPathForRoute(e.rootRoute), )) .toList(); _routeState = StatefulShellRouteState( @@ -132,6 +143,18 @@ class StatefulNavigationShellState extends State { lastRouteInformation: RouteInformation(location: location, state: extra), ); + if (widget.shellRoute.preloadBranches) { + for (int i = 0; i < branchState.length; i++) { + if (i != currentIndex && branchState[i].navigator == null) { + final Navigator? navigator = + widget.branchNavigatorBuilder(context, _routeState, i); + branchState[i] = branchState[i].copy( + navigator: navigator, + ); + } + } + } + _routeState = StatefulShellRouteState( go: _go, route: widget.shellRoute, diff --git a/packages/go_router/lib/src/route.dart b/packages/go_router/lib/src/route.dart index f2d6208863a9..4a47a6e422cc 100644 --- a/packages/go_router/lib/src/route.dart +++ b/packages/go_router/lib/src/route.dart @@ -632,11 +632,13 @@ class StatefulShellRoute extends ShellRouteBase { required List branches, required ShellRouteBuilder builder, ShellRoutePageBuilder? pageBuilder, + bool preloadBranches = false, }) : this._( routes: _rootRoutes(branches), branches: branches, builder: builder, pageBuilder: pageBuilder, + preloadBranches: preloadBranches, ); /// Constructs a [StatefulShellRoute] from a list of [GoRoute]s, each @@ -651,6 +653,7 @@ class StatefulShellRoute extends ShellRouteBase { required List routes, required ShellRouteBuilder builder, ShellRoutePageBuilder? pageBuilder, + bool preloadBranches = false, }) : this._( routes: routes, branches: routes.map((GoRoute e) { @@ -658,6 +661,7 @@ class StatefulShellRoute extends ShellRouteBase { rootRoute: e, navigatorKey: e.parentNavigatorKey); }).toList(), builder: builder, + preloadBranches: preloadBranches, pageBuilder: pageBuilder, ); @@ -665,6 +669,7 @@ class StatefulShellRoute extends ShellRouteBase { required super.routes, required this.branches, required this.builder, + required this.preloadBranches, this.pageBuilder, }) : assert(branches.isNotEmpty), assert(_debugUniqueNavigatorKeys(branches).length == branches.length, @@ -722,6 +727,14 @@ class StatefulShellRoute extends ShellRouteBase { /// built for this route, using the builder function. final ShellRoutePageBuilder? pageBuilder; + /// Whether the route branches should be preloaded when navigating to this + /// route for the first time. + /// + /// If this is true, all the [branches] will be preloaded by navigating to the + /// [ShellRouteBranch.rootRoute], or the route matching + /// [ShellRouteBranch.defaultLocation]. + final bool preloadBranches; + @override GlobalKey? navigatorKeyForSubRoute(RouteBase subRoute) { final int routeIndex = routes.indexOf(subRoute); diff --git a/packages/go_router/lib/src/state.dart b/packages/go_router/lib/src/state.dart index 225e43d9dcc4..e43e8a47f68b 100644 --- a/packages/go_router/lib/src/state.dart +++ b/packages/go_router/lib/src/state.dart @@ -178,7 +178,7 @@ class ShellRouteBranchState { /// Gets the defaultLocation specified in [routeBranch] or falls back to /// the path of the root route of the branch. - String get _defaultLocation => routeBranch.defaultLocation ?? _rootRoutePath; + String get defaultLocation => routeBranch.defaultLocation ?? _rootRoutePath; final RouteInformation? _lastRouteInformation; @@ -190,7 +190,7 @@ class ShellRouteBranchState { /// Returns the last location navigated to on this route branch. If this /// branch hasn't been visited yet, the default location will be used /// (see [ShellRouteBranch.defaultLocation]). - String get _location => _lastRouteInformation?.location ?? _defaultLocation; + String get _location => _lastRouteInformation?.location ?? defaultLocation; @override bool operator ==(Object other) { diff --git a/packages/go_router/test/go_router_test.dart b/packages/go_router/test/go_router_test.dart index 9536fcf5db86..17356acf70be 100644 --- a/packages/go_router/test/go_router_test.dart +++ b/packages/go_router/test/go_router_test.dart @@ -2595,6 +2595,68 @@ void main() { expect(find.text('Screen A'), findsNothing); expect(find.text('Screen B - X'), findsOneWidget); }); + + testWidgets('Preloads routes correctly in a StatefulShellRoute', + (WidgetTester tester) async { + final GlobalKey rootNavigatorKey = + GlobalKey(); + final GlobalKey statefulWidgetKeyA = + GlobalKey(); + final GlobalKey statefulWidgetKeyB = + GlobalKey(); + final GlobalKey statefulWidgetKeyC = + GlobalKey(); + final GlobalKey statefulWidgetKeyD = + GlobalKey(); + + final List routes = [ + StatefulShellRoute.rootRoutes( + builder: (BuildContext context, GoRouterState state, Widget child) => + child, + routes: [ + GoRoute( + path: '/a', + builder: (BuildContext context, GoRouterState state) => + DummyStatefulWidget(key: statefulWidgetKeyA), + ), + GoRoute( + path: '/b', + builder: (BuildContext context, GoRouterState state) => + DummyStatefulWidget(key: statefulWidgetKeyB), + ), + ], + ), + StatefulShellRoute.rootRoutes( + preloadBranches: true, + builder: (BuildContext context, GoRouterState state, Widget child) => + child, + routes: [ + GoRoute( + path: '/c', + builder: (BuildContext context, GoRouterState state) => + DummyStatefulWidget(key: statefulWidgetKeyC), + ), + GoRoute( + path: '/d', + builder: (BuildContext context, GoRouterState state) => + DummyStatefulWidget(key: statefulWidgetKeyD), + ), + ], + ), + ]; + + 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)); + }); }); group('Imperative navigation', () { From 1c509f1dd1ddb33d089445c4ce46a7fd0a7a7f54 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20Lo=CC=88fstrand?= Date: Fri, 28 Oct 2022 15:21:51 +0200 Subject: [PATCH 043/112] Various updates from PR feedback: - Moved builder and pageBuilder back to ShellRouteBase and updated docs of StatefulShellRoute and ShellRouteBase. - Updates in sample code (brought back animation example etc). --- .../lib/stateful_nested_navigation.dart | 73 ++++++++-- .../src/misc/stateful_navigation_shell.dart | 2 +- packages/go_router/lib/src/route.dart | 127 +++++++++--------- packages/go_router/lib/src/state.dart | 17 ++- 4 files changed, 141 insertions(+), 78 deletions(-) diff --git a/packages/go_router/example/lib/stateful_nested_navigation.dart b/packages/go_router/example/lib/stateful_nested_navigation.dart index 4b043de0f6ea..b17b3b2242f8 100644 --- a/packages/go_router/example/lib/stateful_nested_navigation.dart +++ b/packages/go_router/example/lib/stateful_nested_navigation.dart @@ -2,6 +2,7 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; @@ -128,8 +129,8 @@ class NestedTabNavigationExampleApp extends StatelessWidget { ], ), ], - builder: (BuildContext context, GoRouterState state, - Widget ignoredNavigatorContainer) { + builder: + (BuildContext context, GoRouterState state, Widget child) { /// For this nested StatefulShellRoute we are using a custom /// container (TabBarView) for the branch navigators, and thus /// ignoring the default navigator contained passed to the @@ -141,14 +142,22 @@ class NestedTabNavigationExampleApp extends StatelessWidget { ), ), ], - builder: (BuildContext context, GoRouterState state, - Widget navigatorContainer) { - return ScaffoldWithNavBar(body: navigatorContainer); + builder: (BuildContext context, GoRouterState state, Widget child) { + return ScaffoldWithNavBar(body: child); }, + /// If you need to create a custom container for the branch routes, to + /// for instance setup animations, you can implement your builder + /// something like this (see _AnimatedRouteBranchContainer): + // builder: (BuildContext context, GoRouterState state, Widget child) { + // return ScaffoldWithNavBar( + // body: _AnimatedRouteBranchContainer(), + // ); + // }, + /// If you need to customize the Page for StatefulShellRoute, pass a - /// pageProvider function in addition to the builder, for example: - // pageProvider: + /// pageBuilder function in addition to the builder, for example: + // pageBuilder: // (BuildContext context, GoRouterState state, Widget statefulShell) { // return NoTransitionPage(child: statefulShell); // }, @@ -298,7 +307,10 @@ class DetailsScreenState extends State { body: _build(context), ); } else { - return _build(context); + return Container( + color: Theme.of(context).scaffoldBackgroundColor, + child: _build(context), + ); } } @@ -350,9 +362,10 @@ class TabbedRootScreen extends StatelessWidget { @override Widget build(BuildContext context) { final StatefulShellRouteState shellState = StatefulShellRoute.of(context); - final int branchCount = shellState.branchState.length; - final List children = List.generate( - branchCount, (int index) => TabbedRootScreenTab(index: index)); + final List children = shellState.branchState + .mapIndexed((int index, ShellRouteBranchState e) => + TabbedRootScreenTab(index: index)) + .toList(); return DefaultTabController( length: 2, @@ -361,8 +374,7 @@ class TabbedRootScreen extends StatelessWidget { appBar: AppBar( title: const Text('Tab root'), bottom: TabBar( - tabs: List.generate( - branchCount, (int i) => Tab(text: 'Tab ${i + 1}')), + tabs: children.map((TabbedRootScreenTab e) => e.tab).toList(), onTap: (int tappedIndex) => _onTabTap(context, shellState, tappedIndex), )), @@ -382,7 +394,9 @@ class TabbedRootScreen extends StatelessWidget { /// Widget wrapping the [Navigator] for a specific tab in [TabbedRootScreen]. /// /// This class is needed since [TabBarView] won't update its cached list of -/// children while in a transition between tabs. +/// children while in a transition between tabs. This is why we only pass the +/// index of the branch as a parameter, and fetch the state fresh in the build +/// method. class TabbedRootScreenTab extends StatelessWidget { /// Constructs a TabbedRootScreenTab const TabbedRootScreenTab({Key? key, required this.index}) : super(key: key); @@ -390,8 +404,13 @@ class TabbedRootScreenTab extends StatelessWidget { /// The index of the tab final int index; + /// Gets the associated [Tab] object + Tab get tab => Tab(text: 'Tab ${index + 1}'); + @override Widget build(BuildContext context) { + // Note that we must fetch the state fresh here, since the + // TabbedRootScreenTab is "cached" by the TabBarView. final StatefulShellRouteState shellState = StatefulShellRoute.of(context); final Widget? navigator = shellState.branchState[index].navigator; return navigator ?? const SizedBox.expand(); @@ -435,3 +454,29 @@ class TabScreen extends StatelessWidget { ); } } + +// ignore: unused_element +class _AnimatedRouteBranchContainer extends StatelessWidget { + @override + Widget build(BuildContext context) { + final StatefulShellRouteState shellRouteState = + StatefulShellRoute.of(context); + return Stack( + children: shellRouteState.navigators.mapIndexed( + (int index, Widget? navigator) { + return AnimatedScale( + scale: index == shellRouteState.index ? 1 : 1.5, + duration: const Duration(milliseconds: 400), + child: AnimatedOpacity( + opacity: index == shellRouteState.index ? 1 : 0, + duration: const Duration(milliseconds: 400), + child: Offstage( + offstage: index != shellRouteState.index, + child: navigator ?? const SizedBox.shrink(), + ), + ), + ); + }, + ).toList()); + } +} diff --git a/packages/go_router/lib/src/misc/stateful_navigation_shell.dart b/packages/go_router/lib/src/misc/stateful_navigation_shell.dart index 120f05cedf1c..80b8600e5bee 100644 --- a/packages/go_router/lib/src/misc/stateful_navigation_shell.dart +++ b/packages/go_router/lib/src/misc/stateful_navigation_shell.dart @@ -170,7 +170,7 @@ class StatefulNavigationShellState extends State { child: Builder(builder: (BuildContext context) { // This Builder Widget is mainly used to make it possible to access the // StatefulShellRouteState via the BuildContext in the ShellRouteBuilder - final ShellRouteBuilder shellRouteBuilder = widget.shellRoute.builder; + final ShellRouteBuilder shellRouteBuilder = widget.shellRoute.builder!; return shellRouteBuilder( context, widget.shellGoRouterState, diff --git a/packages/go_router/lib/src/route.dart b/packages/go_router/lib/src/route.dart index 4a47a6e422cc..9bd26b46c96a 100644 --- a/packages/go_router/lib/src/route.dart +++ b/packages/go_router/lib/src/route.dart @@ -337,7 +337,24 @@ class GoRoute extends RouteBase { /// Base class for classes that acts as a shell for sub-routes, such /// as [ShellRoute] and [StatefulShellRoute]. abstract class ShellRouteBase extends RouteBase { - const ShellRouteBase._({super.routes}) : super._(); + const ShellRouteBase._({this.builder, this.pageBuilder, super.routes}) + : super._(); + + /// The widget builder for a shell route. + /// + /// 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. + final ShellRouteBuilder? builder; + + /// The page builder for a shell route. + /// + /// 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. + final ShellRoutePageBuilder? pageBuilder; /// Returns the key for the [Navigator] that is to be used for the specified /// immediate sub-route of this shell route. @@ -442,8 +459,8 @@ abstract class ShellRouteBase extends RouteBase { class ShellRoute extends ShellRouteBase { /// Constructs a [ShellRoute]. ShellRoute({ - this.builder, - this.pageBuilder, + super.builder, + super.pageBuilder, super.routes, GlobalKey? navigatorKey, this.restorationScopeId, @@ -458,20 +475,6 @@ class ShellRoute extends ShellRouteBase { } } - /// The widget builder for a shell route. - /// - /// Similar to GoRoute builder, but with an additional child parameter. This - /// child parameter is the [Navigator] that will be used for the matching - /// sub-routes. - final ShellRouteBuilder? builder; - - /// The page builder for a shell route. - /// - /// Similar to GoRoute pageBuilder, but with an additional child parameter. - /// This child parameter is the [Navigator] that will be used for the matching - /// sub-routes. - final ShellRoutePageBuilder? pageBuilder; - /// The [GlobalKey] to be used by the [Navigator] built for this route. /// All ShellRoutes build a Navigator by default. Child GoRoutes /// are placed onto this Navigator instead of the root Navigator. @@ -496,10 +499,10 @@ class ShellRoute extends ShellRouteBase { /// Similar to [ShellRoute], this route class places its sub-route on a /// different Navigator than the root Navigator. However, this route class /// differs in that it creates separate Navigators for each of its nested -/// route branches (route trees), making it possible to build a stateful -/// nested navigation. This is convenient when for instance implementing a UI -/// with a [BottomNavigationBar], with a persistent navigation state for each -/// tab. +/// route branches (route trees), making it possible to build an app with +/// stateful nested navigation. This is convenient when for instance +/// implementing a UI with a [BottomNavigationBar], with a persistent navigation +/// state for each tab. /// /// A StatefulShellRoute is created by specifying a List of [ShellRouteBranch] /// items, each representing a separate stateful branch in the route tree. @@ -510,6 +513,39 @@ class ShellRoute extends ShellRouteBase { /// this shorthand constructor, the [GoRoute.parentNavigatorKey] will be used /// as the Navigator key. /// +/// Like [ShellRoute], you can provide a builder ([ShellRouteBranchState]) and +/// pageBuilder ([ShellRoutePageBuilder]) when creating a StatefulShellRoute. +/// However, StatefulShellRoute differs in that the builder is mandatory and the +/// pageBuilder will be used in addition to the builder. The child parameters of +/// the builders are also a bit different, even though this should normally not +/// affect how you implemented the builders. +/// +/// For the pageBuilder, the child parameter will simply be the stateful shell +/// already built for this route, using the builder function. In the builder +/// function however, the child parameter is a Widget that contains - and is +/// responsible for managing - the Navigators for the different route branches +/// of this StatefulShellRoute. This widget is meant to be used as the body of a +/// custom shell implementation, for example as the body of [Scaffold] with a +/// [BottomNavigationBar]. +/// +/// The builder function of a StatefulShellRoute will be invoked from within a +/// wrapper Widget that provides access to the current [StatefulShellRouteState] +/// associated with the route (via the method [StatefulShellRoute.of]). That +/// state object exposes information such as the current branch index, the state +/// of the route branches etc. +/// +/// For implementations where greater control is needed over the layout and +/// animations of the Navigators, the child parameter in builder can be ignored, +/// and a custom implementation can instead be built by using the Navigators +/// (and other information from StatefulShellRouteState) directly. For example: +/// +/// ``` +/// final StatefulShellRouteState shellState = StatefulShellRoute.of(context); +/// final int currentIndex = shellState.currentBranchIndex; +/// final List navigators = shellRouteState.branchNavigators; +/// return MyCustomShell(currentIndex, navigators); +/// ``` +/// /// Below is a simple example of how a router configuration with /// StatefulShellRoute could be achieved. In this example, a /// BottomNavigationBar with two tabs is used, and each of the tabs gets its @@ -627,7 +663,10 @@ class StatefulShellRoute extends ShellRouteBase { /// representing a root in a stateful route branch. /// /// A separate [Navigator] will be created for each of the branches, using - /// the navigator key specified in [ShellRouteBranch]. + /// the navigator key specified in [ShellRouteBranch]. Note that unlike + /// [ShellRoute], you must always provide a builder when creating + /// a StatefulShellRoute. The pageBuilder however is optional, and is used + /// in addition to the builder. StatefulShellRoute({ required List branches, required ShellRouteBuilder builder, @@ -649,6 +688,10 @@ class StatefulShellRoute extends ShellRouteBase { /// [ShellRouteBranch]s. Each GoRoute provides the navigator key /// (via [GoRoute.parentNavigatorKey]) that will be used to create the /// separate [Navigator]s for the routes. + /// + /// Note that unlike [ShellRoute], you must always provide a builder when + /// creating a StatefulShellRoute. The pageBuilder however is optional, and is + /// used in addition to the builder. StatefulShellRoute.rootRoutes({ required List routes, required ShellRouteBuilder builder, @@ -668,9 +711,9 @@ class StatefulShellRoute extends ShellRouteBase { StatefulShellRoute._({ required super.routes, required this.branches, - required this.builder, + required super.builder, required this.preloadBranches, - this.pageBuilder, + super.pageBuilder, }) : assert(branches.isNotEmpty), assert(_debugUniqueNavigatorKeys(branches).length == branches.length, 'Navigator keys must be unique'), @@ -691,42 +734,6 @@ class StatefulShellRoute extends ShellRouteBase { /// and the route that will be used as the root of the route branch. final List branches; - /// The widget builder for a stateful shell route. - /// - /// Similar to GoRoute builder, but with an additional child parameter. This - /// child parameter is a Widget that contains - and is responsible for - /// managing - the Navigators for the different route branches of this - /// StatefulShellRoute. This widget is meant to be used as the body of a - /// custom shell implementation, for example as the body of [Scaffold] with a - /// [BottomNavigationBar]. - /// - /// The builder function of a StatefulShellRoute will be invoked from within a - /// wrapper Widget that provides access to the current - /// [StatefulShellRouteState] associated with the route (via the method - /// [StatefulShellRoute.of]). That state object exposes information such as - /// the current branch index, the state of the route branches etc. - /// - /// For implementations where greater control is needed over the layout and - /// animations of the Navigators, the child parameter can be ignored, and a - /// custom implementation can instead be built by using the Navigators (and - /// other information from StatefulShellRouteState) directly. For example: - /// - /// ``` - /// final StatefulShellRouteState shellState = StatefulShellRoute.of(context); - /// final int currentIndex = shellState.currentBranchIndex; - /// final List navigators = shellRouteState.branchNavigators; - /// return MyCustomShell(currentIndex, navigators); - /// ``` - final ShellRouteBuilder builder; - - /// Function for customizing the [Page] for this stateful shell. - /// - /// Similar to GoRoute pageBuilder, but with an additional child parameter. - /// Unlike GoRoute however, this function is used in combination with - /// [builder], and the child parameter will be the stateful shell already - /// built for this route, using the builder function. - final ShellRoutePageBuilder? pageBuilder; - /// Whether the route branches should be preloaded when navigating to this /// route for the first time. /// diff --git a/packages/go_router/lib/src/state.dart b/packages/go_router/lib/src/state.dart index e43e8a47f68b..682bbad3cef2 100644 --- a/packages/go_router/lib/src/state.dart +++ b/packages/go_router/lib/src/state.dart @@ -85,7 +85,12 @@ class GoRouterState { } } -/// The current state for a [StatefulShellRoute]. +/// The snapshot of the current state of a [StatefulShellRoute]. +/// +/// Note that this an immutable class, that represents the snapshot of the state +/// of a StatefulShellRoute at a given point in time. Therefore, instances of +/// this object should not be stored, but instead fetched fresh when needed, +/// using the method [StatefulShellRoute.of]. @immutable class StatefulShellRouteState { /// Constructs a [StatefulShellRouteState]. @@ -142,8 +147,14 @@ class StatefulShellRouteState { int get hashCode => Object.hash(route, branchState, index); } -/// The current state for a particular route branch -/// ([ShellRouteBranch]) of a [StatefulShellRoute]. +/// The snapshot of the current state for a particular route branch +/// ([ShellRouteBranch]) in a [StatefulShellRoute]. +/// +/// Note that this an immutable class, that represents the snapshot of the state +/// of a ShellRouteBranchState at a given point in time. Therefore, instances of +/// this object should not be stored, but instead fetched fresh when needed, +/// via the [StatefulShellRouteState] returned by the method +/// [StatefulShellRoute.of]. @immutable class ShellRouteBranchState { /// Constructs a [ShellRouteBranchState]. From 42c7b7d183412e79f3302bf2c2155befc51ff2d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20Lo=CC=88fstrand?= Date: Sat, 29 Oct 2022 14:03:12 +0200 Subject: [PATCH 044/112] Changed the way switching between route branches works - switching is now done by replacing the current RouteMatchList in GoRouterDelegate instead of navigating via GoRouter.go. --- .../lib/stateful_nested_navigation.dart | 2 +- packages/go_router/lib/src/delegate.dart | 6 +++ .../src/misc/stateful_navigation_shell.dart | 18 ++++---- packages/go_router/lib/src/state.dart | 45 +++++++++---------- 4 files changed, 38 insertions(+), 33 deletions(-) diff --git a/packages/go_router/example/lib/stateful_nested_navigation.dart b/packages/go_router/example/lib/stateful_nested_navigation.dart index b17b3b2242f8..3350d08e0400 100644 --- a/packages/go_router/example/lib/stateful_nested_navigation.dart +++ b/packages/go_router/example/lib/stateful_nested_navigation.dart @@ -368,7 +368,7 @@ class TabbedRootScreen extends StatelessWidget { .toList(); return DefaultTabController( - length: 2, + length: children.length, initialIndex: shellState.index, child: Scaffold( appBar: AppBar( diff --git a/packages/go_router/lib/src/delegate.dart b/packages/go_router/lib/src/delegate.dart index d9af2d79e9fa..b282431b0ac3 100644 --- a/packages/go_router/lib/src/delegate.dart +++ b/packages/go_router/lib/src/delegate.dart @@ -179,6 +179,12 @@ class GoRouterDelegate extends RouterDelegate notifyListeners(); } + /// Replaces the entire page stack. + void replaceMatchList(RouteMatchList matchList) { + _matchList = matchList; + notifyListeners(); + } + /// For internal use; visible for testing only. @visibleForTesting RouteMatchList get matches => _matchList; diff --git a/packages/go_router/lib/src/misc/stateful_navigation_shell.dart b/packages/go_router/lib/src/misc/stateful_navigation_shell.dart index 80b8600e5bee..95540c933930 100644 --- a/packages/go_router/lib/src/misc/stateful_navigation_shell.dart +++ b/packages/go_router/lib/src/misc/stateful_navigation_shell.dart @@ -94,8 +94,13 @@ class StatefulNavigationShellState extends State { return index < 0 ? 0 : index; } - void _go(String location, Object? extra) { - GoRouter.of(context).go(location, extra: extra); + void _switchActiveBranch( + ShellRouteBranchState branchState, RouteMatchList? matchList) { + if (matchList != null) { + GoRouter.of(context).routerDelegate.replaceMatchList(matchList); + } else { + GoRouter.of(context).go(branchState.defaultLocation); + } } String _fullPathForRoute(RouteBase route) => @@ -111,7 +116,7 @@ class StatefulNavigationShellState extends State { )) .toList(); _routeState = StatefulShellRouteState( - go: _go, + switchActiveBranch: _switchActiveBranch, route: widget.shellRoute, branchState: branchState, index: 0, @@ -132,15 +137,12 @@ class StatefulNavigationShellState extends State { void _updateRouteState() { final int currentIndex = _findCurrentIndex(); - final RouteMatchList matchList = widget.matchList; - final String location = matchList.location.toString(); - final Object? extra = matchList.extra; final List branchState = _routeState.branchState.toList(); branchState[currentIndex] = branchState[currentIndex].copy( navigator: widget.navigator, - lastRouteInformation: RouteInformation(location: location, state: extra), + matchList: widget.matchList, ); if (widget.shellRoute.preloadBranches) { @@ -156,7 +158,7 @@ class StatefulNavigationShellState extends State { } _routeState = StatefulShellRouteState( - go: _go, + switchActiveBranch: _switchActiveBranch, route: widget.shellRoute, branchState: branchState, index: currentIndex, diff --git a/packages/go_router/lib/src/state.dart b/packages/go_router/lib/src/state.dart index 682bbad3cef2..52b40529670d 100644 --- a/packages/go_router/lib/src/state.dart +++ b/packages/go_router/lib/src/state.dart @@ -6,7 +6,7 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/widgets.dart'; import 'configuration.dart'; -import 'router.dart'; +import 'matching.dart'; /// The route state during routing. /// @@ -95,11 +95,12 @@ class GoRouterState { class StatefulShellRouteState { /// Constructs a [StatefulShellRouteState]. const StatefulShellRouteState({ - required Function(String, Object?) go, + required Function(ShellRouteBranchState, RouteMatchList?) + switchActiveBranch, required this.route, required this.branchState, required this.index, - }) : _go = go; + }) : _switchActiveBranch = switchActiveBranch; /// The associated [StatefulShellRoute] final StatefulShellRoute route; @@ -111,7 +112,7 @@ class StatefulShellRouteState { /// The index of the currently active route branch. final int index; - final Function(String, Object?) _go; + final Function(ShellRouteBranchState, RouteMatchList?) _switchActiveBranch; /// Gets the [Navigator]s for each of the route branches. /// @@ -123,11 +124,13 @@ class StatefulShellRouteState { /// Navigate to the current location of the branch with the provided index. /// /// This method will switch the currently active [Navigator] for the - /// [StatefulShellRoute] by navigating to the current location of the - /// specified branch, using the method [GoRouter.go]. - void goBranch(int index, {Object? extra}) { - _go(branchState[index]._location, - extra ?? branchState[index]._lastRouteInformation?.state); + /// [StatefulShellRoute] by replacing the current navigation stack with the + /// one of the route branch at the provided index. If resetLocation is true, + /// the branch will be reset to its default location (see + /// [ShellRouteBranchState.defaultLocation]). + void goBranch(int index, {bool resetLocation = false}) { + _switchActiveBranch(branchState[index], + resetLocation ? null : branchState[index]._matchList); } @override @@ -162,19 +165,19 @@ class ShellRouteBranchState { required this.routeBranch, required String rootRoutePath, this.navigator, - RouteInformation? lastRouteInformation, - }) : _lastRouteInformation = lastRouteInformation, + RouteMatchList? matchList, + }) : _matchList = matchList, _rootRoutePath = rootRoutePath; /// Constructs a copy of this [ShellRouteBranchState], with updated values for /// some of the fields. ShellRouteBranchState copy( - {Navigator? navigator, RouteInformation? lastRouteInformation}) { + {Navigator? navigator, RouteMatchList? matchList}) { return ShellRouteBranchState( routeBranch: routeBranch, rootRoutePath: _rootRoutePath, navigator: navigator ?? this.navigator, - lastRouteInformation: lastRouteInformation ?? _lastRouteInformation, + matchList: matchList ?? _matchList, ); } @@ -191,18 +194,12 @@ class ShellRouteBranchState { /// the path of the root route of the branch. String get defaultLocation => routeBranch.defaultLocation ?? _rootRoutePath; - final RouteInformation? _lastRouteInformation; + /// The current navigation stack for the branch. + final RouteMatchList? _matchList; /// The full path at which root route for the route branch is reachable. final String _rootRoutePath; - /// Gets the current location for this branch. - /// - /// Returns the last location navigated to on this route branch. If this - /// branch hasn't been visited yet, the default location will be used - /// (see [ShellRouteBranch.defaultLocation]). - String get _location => _lastRouteInformation?.location ?? defaultLocation; - @override bool operator ==(Object other) { if (identical(other, this)) { @@ -214,10 +211,10 @@ class ShellRouteBranchState { return other.routeBranch == routeBranch && other._rootRoutePath == _rootRoutePath && other.navigator == navigator && - other._lastRouteInformation == _lastRouteInformation; + other._matchList == _matchList; } @override - int get hashCode => Object.hash( - routeBranch, _rootRoutePath, navigator, _lastRouteInformation); + int get hashCode => + Object.hash(routeBranch, _rootRoutePath, navigator, _matchList); } From 62e7fc15dc60d0ad3df0540b0c97e93a2337bff5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20Lo=CC=88fstrand?= Date: Sun, 30 Oct 2022 20:57:38 +0100 Subject: [PATCH 045/112] Reverted restriction around pushing sub-routes of a StatefulShellRoute. Unit test updates. --- .../lib/stateful_nested_navigation.dart | 8 +-- packages/go_router/lib/src/configuration.dart | 49 ++------------ packages/go_router/lib/src/delegate.dart | 9 --- .../go_router/test/configuration_test.dart | 47 ------------- packages/go_router/test/delegate_test.dart | 52 ++------------- packages/go_router/test/go_router_test.dart | 66 +++++++++++++++++++ 6 files changed, 80 insertions(+), 151 deletions(-) diff --git a/packages/go_router/example/lib/stateful_nested_navigation.dart b/packages/go_router/example/lib/stateful_nested_navigation.dart index 3350d08e0400..c98c00eac88e 100644 --- a/packages/go_router/example/lib/stateful_nested_navigation.dart +++ b/packages/go_router/example/lib/stateful_nested_navigation.dart @@ -201,16 +201,10 @@ class ScaffoldWithNavBar extends StatelessWidget { BottomNavigationBarItem(icon: Icon(Icons.tab), label: 'Section C'), ], currentIndex: shellState.index, - onTap: (int tappedIndex) => - _onItemTapped(context, shellState, tappedIndex), + onTap: (int tappedIndex) => shellState.goBranch(tappedIndex), ), ); } - - void _onItemTapped( - BuildContext context, StatefulShellRouteState shellState, int index) { - shellState.goBranch(index); - } } /// Widget for the root/initial pages in the bottom navigation bar. diff --git a/packages/go_router/lib/src/configuration.dart b/packages/go_router/lib/src/configuration.dart index 00019bb4d7af..ae6830f50479 100644 --- a/packages/go_router/lib/src/configuration.dart +++ b/packages/go_router/lib/src/configuration.dart @@ -107,13 +107,13 @@ class RouteConfiguration { if (branch.defaultLocation == null) { continue; } - final RouteBase defaultLocationRoute = - matcher.findMatch(branch.defaultLocation!).last.route; - - assert(branch.rootRoute == defaultLocationRoute || - isDescendantOrSame( - ancestor: branch.rootRoute, - route: defaultLocationRoute)); + final RouteMatchList matchList = + matcher.findMatch(branch.defaultLocation!); + assert( + matchList.isNotEmpty, + 'defaultLocation ' + '(${branch.defaultLocation}) of ShellRouteBranch must be a ' + 'valid location'); } } checkShellRouteBranchDefaultLocations(route.routes, matcher); @@ -213,41 +213,6 @@ class RouteConfiguration { return null; } - /// Finds the root route of closest ShellRouteBranch ancestor of the provided - /// route. - RouteBase? findAncestorShellRouteBranchRoute( - RouteBase route) { - final List ancestors = _ancestorsForRoute(route, routes); - final int shellRouteIndex = - ancestors.lastIndexWhere((RouteBase e) => e is StatefulShellRoute); - if (shellRouteIndex >= 0 && shellRouteIndex < (ancestors.length - 1)) { - return ancestors[shellRouteIndex + 1]; - } - return null; - } - - /// Tests if a route is a descendant of, or same as, an ancestor route. - bool isDescendantOrSame( - {required RouteBase ancestor, required RouteBase route}) { - return _ancestorsForRoute(route, routes).contains(ancestor); - } - - static List _ancestorsForRoute( - RouteBase targetRoute, List routes) { - for (final RouteBase route in routes) { - if (route.routes.contains(targetRoute)) { - return [route, targetRoute]; - } else { - final List ancestors = - _ancestorsForRoute(targetRoute, route.routes); - if (ancestors.isNotEmpty) { - return [route, ...ancestors]; - } - } - } - return []; - } - @override String toString() { return 'RouterConfiguration: $routes'; diff --git a/packages/go_router/lib/src/delegate.dart b/packages/go_router/lib/src/delegate.dart index b282431b0ac3..cab2025f2f62 100644 --- a/packages/go_router/lib/src/delegate.dart +++ b/packages/go_router/lib/src/delegate.dart @@ -100,15 +100,6 @@ class GoRouterDelegate extends RouterDelegate if (match.route is ShellRouteBase) { throw GoError('ShellRoutes cannot be pushed'); } - final RouteBase? ancestorBranchRootRoute = - _configuration.findAncestorShellRouteBranchRoute(_matchList.last.route); - if (ancestorBranchRootRoute != null) { - if (!_configuration.isDescendantOrSame( - ancestor: ancestorBranchRootRoute, route: match.route)) { - throw GoError('Cannot push a route that is not a descendant of the ' - 'current StatefulShellRoute branch'); - } - } // Remap the pageKey to allow any number of the same page on the stack final String fullPath = match.fullpath; diff --git a/packages/go_router/test/configuration_test.dart b/packages/go_router/test/configuration_test.dart index a6a8d76982c0..97ef7c8db8f0 100644 --- a/packages/go_router/test/configuration_test.dart +++ b/packages/go_router/test/configuration_test.dart @@ -237,53 +237,6 @@ void main() { ); }); - test( - 'throws when a branch of a StatefulShellRoute has a defaultLocation ' - 'that belongs to another branch', () { - final GlobalKey root = - GlobalKey(debugLabel: 'root'); - final GlobalKey sectionANavigatorKey = - GlobalKey(); - final GlobalKey sectionBNavigatorKey = - GlobalKey(); - expect( - () { - RouteConfiguration( - navigatorKey: root, - routes: [ - StatefulShellRoute(branches: [ - ShellRouteBranch( - defaultLocation: '/b', - navigatorKey: sectionANavigatorKey, - rootRoute: GoRoute( - path: '/a', - builder: _mockScreenBuilder, - ), - ), - ShellRouteBranch( - defaultLocation: '/b', - navigatorKey: sectionBNavigatorKey, - rootRoute: StatefulShellRoute(branches: [ - ShellRouteBranch( - rootRoute: GoRoute( - path: '/b', - builder: _mockScreenBuilder, - ), - ), - ], builder: _mockShellBuilder), - ), - ], builder: _mockShellBuilder), - ], - redirectLimit: 10, - topRedirect: (BuildContext context, GoRouterState state) { - return null; - }, - ); - }, - throwsAssertionError, - ); - }); - test( 'does not throw when a branch of a StatefulShellRoute has correctly ' 'configured defaultLocations', () { diff --git a/packages/go_router/test/delegate_test.dart b/packages/go_router/test/delegate_test.dart index 588664718d5c..1c89e1168cca 100644 --- a/packages/go_router/test/delegate_test.dart +++ b/packages/go_router/test/delegate_test.dart @@ -7,7 +7,6 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:go_router/go_router.dart'; import 'package:go_router/src/match.dart'; import 'package:go_router/src/misc/error_screen.dart'; -import 'package:go_router/src/misc/errors.dart'; Future createGoRouter( WidgetTester tester, { @@ -122,61 +121,22 @@ void main() { ); testWidgets( - 'It should throw GoError if pushing a route that is descendant of a ' - 'different StatefulShellRoute branch', - (WidgetTester tester) async { - final GoRouter goRouter = - await createGoRouterWithStatefulShellRoute(tester); - goRouter.push('/c/c1'); - await tester.pumpAndSettle(); - - expect( - () => goRouter.push('/d/d1'), - throwsA(isA()), - ); - await tester.pumpAndSettle(); - }, - ); - - testWidgets( - 'It should throw GoError if pushing a route that is not descendant of ' - 'the current StatefulShellRoute', - (WidgetTester tester) async { - final GoRouter goRouter = - await createGoRouterWithStatefulShellRoute(tester); - goRouter.push('/c/c1'); - await tester.pumpAndSettle(); - - expect( - () => goRouter.push('/a'), - throwsA(isA()), - ); - }, - ); - - testWidgets( - 'It should throw GoError if pushing a route that belongs to a different ' + 'It should successfully push a route from outside the the current ' 'StatefulShellRoute', (WidgetTester tester) async { final GoRouter goRouter = await createGoRouterWithStatefulShellRoute(tester); - goRouter.push('/c'); - await tester.pumpAndSettle(); - - expect( - () => goRouter.push('/d'), - throwsA(isA()), - ); + goRouter.push('/c/c1'); await tester.pumpAndSettle(); - goRouter.push('/c/c1'); + goRouter.push('/a'); await tester.pumpAndSettle(); + expect(goRouter.routerDelegate.matches.matches.length, 3); expect( - () => goRouter.push('/a'), - throwsA(isA()), + goRouter.routerDelegate.matches.matches[2].pageKey, + const Key('/a-p1'), ); - await tester.pumpAndSettle(); }, ); diff --git a/packages/go_router/test/go_router_test.dart b/packages/go_router/test/go_router_test.dart index 17356acf70be..a899e19ff9bd 100644 --- a/packages/go_router/test/go_router_test.dart +++ b/packages/go_router/test/go_router_test.dart @@ -2596,6 +2596,72 @@ void main() { expect(find.text('Screen B - X'), findsOneWidget); }); + testWidgets( + 'Pushed non-descendant routes are correctly restored when ' + 'navigating between branches in StatefulShellRoute', + (WidgetTester tester) async { + final GlobalKey rootNavigatorKey = + GlobalKey(); + final GlobalKey sectionANavigatorKey = + GlobalKey(); + final GlobalKey sectionBNavigatorKey = + GlobalKey(); + StatefulShellRouteState? routeState; + + final List routes = [ + GoRoute( + path: '/common', + builder: (BuildContext context, GoRouterState state) => + Text('Common - ${state.extra}'), + ), + StatefulShellRoute.rootRoutes( + builder: (BuildContext context, GoRouterState state, Widget child) { + routeState = StatefulShellRoute.of(context); + return child; + }, + routes: [ + GoRoute( + parentNavigatorKey: sectionANavigatorKey, + path: '/a', + builder: (BuildContext context, GoRouterState state) => + const Text('Screen A'), + ), + GoRoute( + parentNavigatorKey: sectionBNavigatorKey, + path: '/b', + builder: (BuildContext context, GoRouterState state) => + const Text('Screen B'), + ), + ], + ), + ]; + + final GoRouter router = await createRouter(routes, tester, + initialLocation: '/a', navigatorKey: rootNavigatorKey); + expect(find.text('Screen A'), findsOneWidget); + + router.go('/b'); + await tester.pumpAndSettle(); + expect(find.text('Screen A'), findsNothing); + expect(find.text('Screen B'), findsOneWidget); + + router.push('/common', extra: 'X'); + await tester.pumpAndSettle(); + expect(find.text('Screen A'), findsNothing); + expect(find.text('Screen B'), findsNothing); + expect(find.text('Common - X'), findsOneWidget); + + routeState!.goBranch(0); + await tester.pumpAndSettle(); + expect(find.text('Screen A'), findsOneWidget); + + routeState!.goBranch(1); + await tester.pumpAndSettle(); + expect(find.text('Screen A'), findsNothing); + expect(find.text('Screen B'), findsNothing); + expect(find.text('Common - X'), findsOneWidget); + }); + testWidgets('Preloads routes correctly in a StatefulShellRoute', (WidgetTester tester) async { final GlobalKey rootNavigatorKey = From 9a7069a0e2e8df180ba6c83f52f1bc8aa5b609cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20Lo=CC=88fstrand?= Date: Sun, 30 Oct 2022 20:58:07 +0100 Subject: [PATCH 046/112] Doc fixes/updates for StatefulShellRoute. --- packages/go_router/lib/src/route.dart | 38 +++++++++++++++++---------- 1 file changed, 24 insertions(+), 14 deletions(-) diff --git a/packages/go_router/lib/src/route.dart b/packages/go_router/lib/src/route.dart index 9bd26b46c96a..b20499b6fe5a 100644 --- a/packages/go_router/lib/src/route.dart +++ b/packages/go_router/lib/src/route.dart @@ -513,12 +513,11 @@ class ShellRoute extends ShellRouteBase { /// this shorthand constructor, the [GoRoute.parentNavigatorKey] will be used /// as the Navigator key. /// -/// Like [ShellRoute], you can provide a builder ([ShellRouteBranchState]) and -/// pageBuilder ([ShellRoutePageBuilder]) when creating a StatefulShellRoute. -/// However, StatefulShellRoute differs in that the builder is mandatory and the -/// pageBuilder will be used in addition to the builder. The child parameters of -/// the builders are also a bit different, even though this should normally not -/// affect how you implemented the builders. +/// Like [ShellRoute], you can provide a [builder] and [pageBuilder] when +/// creating a StatefulShellRoute. However, StatefulShellRoute differs in that +/// the builder is mandatory and the pageBuilder will be used in addition to the +/// builder. The child parameters of the builders are also a bit different, even +/// though this should normally not affect how you implemented the builders. /// /// For the pageBuilder, the child parameter will simply be the stateful shell /// already built for this route, using the builder function. In the builder @@ -532,12 +531,23 @@ class ShellRoute extends ShellRouteBase { /// wrapper Widget that provides access to the current [StatefulShellRouteState] /// associated with the route (via the method [StatefulShellRoute.of]). That /// state object exposes information such as the current branch index, the state -/// of the route branches etc. +/// of the route branches etc. It is also with the help this state object you +/// can change the active branch, i.e. restore the navigation stack of another +/// branch. This is accomplished using the method +/// [StatefulShellRouteState.goBranch]. For example: /// -/// For implementations where greater control is needed over the layout and -/// animations of the Navigators, the child parameter in builder can be ignored, -/// and a custom implementation can instead be built by using the Navigators -/// (and other information from StatefulShellRouteState) directly. For example: +/// ``` +/// void _onBottomNavigationBarItemTapped(BuildContext context, int index) { +/// final StatefulShellRouteState shellState = StatefulShellRoute.of(context); +/// shellState.goBranch(index); +/// } +/// ``` +/// +/// Sometimes you need greater control over the layout and animations of the +/// branch Navigators. In such cases, the child argument in the builder function +/// can be ignored, and a custom implementation can instead be built using the +/// branch Navigators (see [StatefulShellRouteState.navigators]) directly. For +/// example: /// /// ``` /// final StatefulShellRouteState shellState = StatefulShellRoute.of(context); @@ -551,7 +561,7 @@ class ShellRoute extends ShellRouteBase { /// BottomNavigationBar with two tabs is used, and each of the tabs gets its /// own Navigator. A container widget responsible for managing the Navigators /// for all route branches will then be passed as the child argument -/// of the [builder] function. +/// of the builder function. /// /// ``` /// final GlobalKey _tabANavigatorKey = @@ -609,8 +619,8 @@ class ShellRoute extends ShellRouteBase { /// ``` /// /// When the [Page] for this route needs to be customized, you need to pass a -/// function for [pageBuilder]. Note that this page provider doesn't replace -/// the [builder] function, but instead receives the stateful shell built by +/// function for pageBuilder. Note that this page builder doesn't replace +/// the builder function, but instead receives the stateful shell built by /// [StatefulShellRoute] (using the builder function) as input. In other words, /// you need to specify both when customizing a page. For example: /// From 141fdc1a51a48e896278b9180c608fdff9b20458 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20Lo=CC=88fstrand?= Date: Fri, 4 Nov 2022 13:24:12 +0100 Subject: [PATCH 047/112] Added handling of redirection when switching and preloading route branches. --- packages/go_router/lib/src/builder.dart | 51 ++----- .../src/misc/stateful_navigation_shell.dart | 126 +++++++++++++----- packages/go_router/lib/src/parser.dart | 14 +- packages/go_router/lib/src/state.dart | 16 ++- packages/go_router/test/go_router_test.dart | 28 +++- 5 files changed, 159 insertions(+), 76 deletions(-) diff --git a/packages/go_router/lib/src/builder.dart b/packages/go_router/lib/src/builder.dart index 45474d477afc..8df3b6f78bcd 100644 --- a/packages/go_router/lib/src/builder.dart +++ b/packages/go_router/lib/src/builder.dart @@ -266,43 +266,20 @@ class RouteBuilder { required VoidCallback pop, }) { return StatefulNavigationShell( - configuration: configuration, - shellRoute: shellRoute, - shellGoRouterState: shellRouterState, - navigator: navigator, - matchList: matchList, - branchNavigatorBuilder: (BuildContext context, - StatefulShellRouteState routeState, int branchIndex) => - _buildPreloadedNavigatorForRouteBranch( - context, routeState, branchIndex, pop), - ); - } - - Navigator? _buildPreloadedNavigatorForRouteBranch(BuildContext context, - StatefulShellRouteState routeState, int branchIndex, VoidCallback pop) { - final ShellRouteBranchState branchState = - routeState.branchState[branchIndex]; - final ShellRouteBranch branch = branchState.routeBranch; - final String defaultLocation = branchState.defaultLocation; - - // Build a RouteMatchList from the default location of the route branch and - // find the index of the branch root route in the match list - RouteMatchList routeMatchList = - RouteMatcher(configuration).findMatch(defaultLocation); - final int routeBranchIndex = routeMatchList.matches - .indexWhere((RouteMatch e) => e.route == branch.rootRoute); - - // Keep only the routes from and below the root route in the match list - if (routeBranchIndex >= 0) { - routeMatchList = - RouteMatchList(routeMatchList.matches.sublist(routeBranchIndex)); - // Build the pages and the Navigator for the route branch - final List> pages = - buildPages(context, routeMatchList, pop, true, branch.navigatorKey); - return _buildNavigator(pop, pages, branch.navigatorKey, - restorationScopeId: branch.restorationScopeId); - } - return null; + configuration: configuration, + shellRoute: shellRoute, + shellGoRouterState: shellRouterState, + navigator: navigator, + matchList: matchList, + branchNavigatorBuilder: (BuildContext context, + RouteMatchList navigatorMatchList, + GlobalKey navigatorKey, + String? restorationScopeId) { + final List> pages = + buildPages(context, navigatorMatchList, pop, true, navigatorKey); + return _buildNavigator(pop, pages, navigatorKey, + restorationScopeId: restorationScopeId); + }); } /// Helper method that builds a [GoRouterState] object for the given [match] diff --git a/packages/go_router/lib/src/misc/stateful_navigation_shell.dart b/packages/go_router/lib/src/misc/stateful_navigation_shell.dart index 95540c933930..e1e809325dd4 100644 --- a/packages/go_router/lib/src/misc/stateful_navigation_shell.dart +++ b/packages/go_router/lib/src/misc/stateful_navigation_shell.dart @@ -6,7 +6,9 @@ import 'package:collection/collection.dart'; import 'package:flutter/widgets.dart'; import '../configuration.dart'; +import '../match.dart'; import '../matching.dart'; +import '../parser.dart'; import '../router.dart'; import '../typedefs.dart'; @@ -31,8 +33,12 @@ class InheritedStatefulNavigationShell extends InheritedWidget { } /// Builder function for a route branch navigator -typedef ShellRouteBranchNavigatorBuilder = Navigator? Function( - BuildContext context, StatefulShellRouteState routeState, int branchIndex); +typedef ShellRouteBranchNavigatorBuilder = Navigator Function( + BuildContext context, + RouteMatchList navigatorMatchList, + GlobalKey navigatorKey, + String? restorationScopeId, +); /// Widget that manages and maintains the state of a [StatefulShellRoute], /// including the [Navigator]s of the configured route branches. @@ -48,7 +54,7 @@ typedef ShellRouteBranchNavigatorBuilder = Navigator? Function( /// provided as the child argument to the builder of the StatefulShellRoute. /// However, implementors can choose to disregard this and use an alternate /// container around the branch navigators -/// (see [StatefulShellRouteState.navigators]) instead. +/// (see [StatefulShellRouteState.children]) instead. class StatefulNavigationShell extends StatefulWidget { /// Constructs an [StatefulNavigationShell]. const StatefulNavigationShell({ @@ -87,6 +93,8 @@ class StatefulNavigationShell extends StatefulWidget { class StatefulNavigationShellState extends State { late StatefulShellRouteState _routeState; + bool _branchesPreloaded = false; + int _findCurrentIndex() { final List branchState = _routeState.branchState; final int index = branchState.indexWhere((ShellRouteBranchState e) => @@ -96,10 +104,73 @@ class StatefulNavigationShellState extends State { void _switchActiveBranch( ShellRouteBranchState branchState, RouteMatchList? matchList) { - if (matchList != null) { - GoRouter.of(context).routerDelegate.replaceMatchList(matchList); + final GoRouter goRouter = GoRouter.of(context); + if (matchList != null && matchList.isNotEmpty) { + goRouter.routeInformationParser + .processRedirection(matchList, context) + .then( + (RouteMatchList matchList) => + goRouter.routerDelegate.replaceMatchList(matchList), + onError: (_) => goRouter.go(branchState.defaultLocation), + ); } else { - GoRouter.of(context).go(branchState.defaultLocation); + goRouter.go(branchState.defaultLocation); + } + } + + Future _preloadBranch( + ShellRouteBranchState branchState) { + // Parse a RouteMatchList from the default location of the route branch and + // handle any redirects + final GoRouteInformationParser parser = + GoRouter.of(context).routeInformationParser; + final Future routeMatchList = + parser.parseRouteInformationWithDependencies( + RouteInformation(location: branchState.defaultLocation), context); + + ShellRouteBranchState createBranchNavigator(RouteMatchList matchList) { + // Find the index of the branch root route in the match list + final ShellRouteBranch branch = branchState.routeBranch; + final int shellRouteIndex = matchList.matches + .indexWhere((RouteMatch e) => e.route == widget.shellRoute); + // Keep only the routes from and below the root route in the match list and + // use that to build the Navigator for the branch + Navigator? navigator; + if (shellRouteIndex >= 0 && + shellRouteIndex < (matchList.matches.length - 1)) { + final RouteMatchList navigatorMatchList = + RouteMatchList(matchList.matches.sublist(shellRouteIndex + 1)); + navigator = widget.branchNavigatorBuilder(context, navigatorMatchList, + branch.navigatorKey, branch.restorationScopeId); + } + return branchState.copy(navigator: navigator, matchList: matchList); + } + + return routeMatchList.then(createBranchNavigator); + } + + void _updateRouteBranchState(int index, ShellRouteBranchState branchState, + {int? currentIndex}) { + final List branchStates = + _routeState.branchState.toList(); + branchStates[index] = branchState; + + _routeState = _routeState.copy( + branchState: branchStates, + index: currentIndex, + ); + } + + void _preloadBranches() { + final List states = _routeState.branchState; + for (int i = 0; i < states.length; i++) { + if (states[i].navigator == null) { + _preloadBranch(states[i]).then((ShellRouteBranchState branchState) { + setState(() { + _updateRouteBranchState(i, branchState); + }); + }); + } } } @@ -126,43 +197,30 @@ class StatefulNavigationShellState extends State { @override void didUpdateWidget(covariant StatefulNavigationShell oldWidget) { super.didUpdateWidget(oldWidget); - _updateRouteState(); + _updateRouteStateFromWidget(); } @override void didChangeDependencies() { super.didChangeDependencies(); - _updateRouteState(); + _updateRouteStateFromWidget(); } - void _updateRouteState() { - final int currentIndex = _findCurrentIndex(); + void _updateRouteStateFromWidget() { + final int index = _findCurrentIndex(); - final List branchState = - _routeState.branchState.toList(); - branchState[currentIndex] = branchState[currentIndex].copy( - navigator: widget.navigator, - matchList: widget.matchList, - ); + _updateRouteBranchState( + index, + _routeState.branchState[index].copy( + navigator: widget.navigator, + matchList: widget.matchList, + ), + currentIndex: index); - if (widget.shellRoute.preloadBranches) { - for (int i = 0; i < branchState.length; i++) { - if (i != currentIndex && branchState[i].navigator == null) { - final Navigator? navigator = - widget.branchNavigatorBuilder(context, _routeState, i); - branchState[i] = branchState[i].copy( - navigator: navigator, - ); - } - } + if (widget.shellRoute.preloadBranches && !_branchesPreloaded) { + _preloadBranches(); + _branchesPreloaded = true; } - - _routeState = StatefulShellRouteState( - switchActiveBranch: _switchActiveBranch, - route: widget.shellRoute, - branchState: branchState, - index: currentIndex, - ); } @override @@ -202,7 +260,7 @@ class _IndexedStackedRouteBranchContainer extends StatelessWidget { Widget _buildRouteBranchContainer( BuildContext context, int index, ShellRouteBranchState routeBranch) { - final Navigator? navigator = routeBranch.navigator; + final Widget? navigator = routeBranch.navigator; if (navigator == null) { return const SizedBox.shrink(); } diff --git a/packages/go_router/lib/src/parser.dart b/packages/go_router/lib/src/parser.dart index 61a89b8d3678..3c39ac343ecc 100644 --- a/packages/go_router/lib/src/parser.dart +++ b/packages/go_router/lib/src/parser.dart @@ -64,6 +64,18 @@ class GoRouteInformationParser extends RouteInformationParser { // still try to process the top-level redirects. initialMatches = RouteMatchList.empty(); } + return processRedirection(initialMatches, context, + topRouteInformation: routeInformation); + } + + /// Processes any redirections for the provided RouteMatchList. + Future processRedirection( + RouteMatchList routeMatchList, BuildContext context, + {RouteInformation? topRouteInformation}) { + final RouteInformation routeInformation = topRouteInformation ?? + RouteInformation( + location: routeMatchList.location.toString(), + state: routeMatchList.extra); Future processRedirectorResult(RouteMatchList matches) { if (matches.isEmpty) { return SynchronousFuture(errorScreen( @@ -76,7 +88,7 @@ class GoRouteInformationParser extends RouteInformationParser { final FutureOr redirectorResult = redirector( context, - SynchronousFuture(initialMatches), + SynchronousFuture(routeMatchList), configuration, matcher, extra: routeInformation.state, diff --git a/packages/go_router/lib/src/state.dart b/packages/go_router/lib/src/state.dart index 52b40529670d..ce75819d2367 100644 --- a/packages/go_router/lib/src/state.dart +++ b/packages/go_router/lib/src/state.dart @@ -102,6 +102,18 @@ class StatefulShellRouteState { required this.index, }) : _switchActiveBranch = switchActiveBranch; + /// Constructs a copy of this [StatefulShellRouteState], with updated values + /// for some of the fields. + StatefulShellRouteState copy( + {List? branchState, int? index}) { + return StatefulShellRouteState( + switchActiveBranch: _switchActiveBranch, + route: route, + branchState: branchState ?? this.branchState, + index: index ?? this.index, + ); + } + /// The associated [StatefulShellRoute] final StatefulShellRoute route; @@ -186,8 +198,8 @@ class ShellRouteBranchState { /// The [Navigator] for this route branch in a [StatefulShellRoute]. /// - /// This field will typically not be set until this route tree has been navigated - /// to at least once. + /// This field will typically not be set until this route tree has been + /// navigated to at least once. final Navigator? navigator; /// Gets the defaultLocation specified in [routeBranch] or falls back to diff --git a/packages/go_router/test/go_router_test.dart b/packages/go_router/test/go_router_test.dart index a899e19ff9bd..f98eee333f31 100644 --- a/packages/go_router/test/go_router_test.dart +++ b/packages/go_router/test/go_router_test.dart @@ -2674,6 +2674,8 @@ void main() { GlobalKey(); final GlobalKey statefulWidgetKeyD = GlobalKey(); + final GlobalKey statefulWidgetKeyE = + GlobalKey(); final List routes = [ StatefulShellRoute.rootRoutes( @@ -2707,12 +2709,33 @@ void main() { builder: (BuildContext context, GoRouterState state) => DummyStatefulWidget(key: statefulWidgetKeyD), ), + 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); + final GoRouter router = await createRouter( + routes, + tester, + initialLocation: '/a', + navigatorKey: rootNavigatorKey, + redirect: (_, GoRouterState state) { + if (state.location == '/e') { + return '/e/details'; + } + return null; + }, + ); expect(statefulWidgetKeyA.currentState?.counter, equals(0)); expect(statefulWidgetKeyB.currentState?.counter, null); expect(statefulWidgetKeyC.currentState?.counter, null); @@ -2722,6 +2745,7 @@ void main() { await tester.pumpAndSettle(); expect(statefulWidgetKeyC.currentState?.counter, equals(0)); expect(statefulWidgetKeyD.currentState?.counter, equals(0)); + expect(statefulWidgetKeyE.currentState?.counter, equals(0)); }); }); From b6b289faec31202cde3c180d3c0c13f4f0c052bb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20Lo=CC=88fstrand?= Date: Fri, 4 Nov 2022 13:24:29 +0100 Subject: [PATCH 048/112] Reintroduced validation of defaultLocation of StatefulShellRoute (in RouteConfiguration). --- packages/go_router/lib/src/configuration.dart | 38 ++++++++++++--- .../go_router/test/configuration_test.dart | 47 +++++++++++++++++++ 2 files changed, 79 insertions(+), 6 deletions(-) diff --git a/packages/go_router/lib/src/configuration.dart b/packages/go_router/lib/src/configuration.dart index ae6830f50479..a54528d3499e 100644 --- a/packages/go_router/lib/src/configuration.dart +++ b/packages/go_router/lib/src/configuration.dart @@ -98,6 +98,8 @@ class RouteConfiguration { checkParentNavigatorKeys( routes, >[navigatorKey]); + // Check to see that the configured defaultLocation of ShellRouteBranches + // points to a descendant route of the route branch. void checkShellRouteBranchDefaultLocations( List routes, RouteMatcher matcher) { try { @@ -107,13 +109,15 @@ class RouteConfiguration { if (branch.defaultLocation == null) { continue; } - final RouteMatchList matchList = - matcher.findMatch(branch.defaultLocation!); + final RouteBase defaultLocationRoute = + matcher.findMatch(branch.defaultLocation!).last.route; assert( - matchList.isNotEmpty, - 'defaultLocation ' - '(${branch.defaultLocation}) of ShellRouteBranch must be a ' - 'valid location'); + _debugIsDescendantOrSame( + ancestor: branch.rootRoute, + route: defaultLocationRoute), + 'The defaultLocation (${branch.defaultLocation}) of ' + 'ShellRouteBranch must match a descendant route of the ' + 'branch'); } } checkShellRouteBranchDefaultLocations(route.routes, matcher); @@ -213,6 +217,28 @@ class RouteConfiguration { return null; } + /// Tests if a route is a descendant of, or same as, an ancestor route. + bool _debugIsDescendantOrSame( + {required RouteBase ancestor, required RouteBase route}) { + return _debugAncestorsForRoute(route, routes).contains(ancestor); + } + + static List _debugAncestorsForRoute( + RouteBase targetRoute, List routes) { + for (final RouteBase route in routes) { + if (route.routes.contains(targetRoute)) { + return [route, targetRoute]; + } else { + final List ancestors = + _debugAncestorsForRoute(targetRoute, route.routes); + if (ancestors.isNotEmpty) { + return [route, ...ancestors]; + } + } + } + return []; + } + @override String toString() { return 'RouterConfiguration: $routes'; diff --git a/packages/go_router/test/configuration_test.dart b/packages/go_router/test/configuration_test.dart index 97ef7c8db8f0..be230b3c3ba5 100644 --- a/packages/go_router/test/configuration_test.dart +++ b/packages/go_router/test/configuration_test.dart @@ -237,6 +237,53 @@ void main() { ); }); + test( + 'throws when a branch of a StatefulShellRoute has a defaultLocation ' + 'that is not a descendant of the same branch', () { + final GlobalKey root = + GlobalKey(debugLabel: 'root'); + final GlobalKey sectionANavigatorKey = + GlobalKey(); + final GlobalKey sectionBNavigatorKey = + GlobalKey(); + expect( + () { + RouteConfiguration( + navigatorKey: root, + routes: [ + StatefulShellRoute(branches: [ + ShellRouteBranch( + defaultLocation: '/b', + navigatorKey: sectionANavigatorKey, + rootRoute: GoRoute( + path: '/a', + builder: _mockScreenBuilder, + ), + ), + ShellRouteBranch( + defaultLocation: '/b', + navigatorKey: sectionBNavigatorKey, + rootRoute: StatefulShellRoute(branches: [ + ShellRouteBranch( + rootRoute: GoRoute( + path: '/b', + builder: _mockScreenBuilder, + ), + ), + ], builder: _mockShellBuilder), + ), + ], builder: _mockShellBuilder), + ], + redirectLimit: 10, + topRedirect: (BuildContext context, GoRouterState state) { + return null; + }, + ); + }, + throwsAssertionError, + ); + }); + test( 'does not throw when a branch of a StatefulShellRoute has correctly ' 'configured defaultLocations', () { From d4edd47b98d767313fa1e93b80e95be407f2426a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20Lo=CC=88fstrand?= Date: Fri, 4 Nov 2022 15:07:34 +0100 Subject: [PATCH 049/112] Removed replaceMatchList in GoRouterDelegate (using setNewRoutePath instead). Some cleanup in RouteBuilder. --- packages/go_router/lib/src/builder.dart | 21 +++++++------------ packages/go_router/lib/src/delegate.dart | 6 ------ .../src/misc/stateful_navigation_shell.dart | 2 +- 3 files changed, 9 insertions(+), 20 deletions(-) diff --git a/packages/go_router/lib/src/builder.dart b/packages/go_router/lib/src/builder.dart index 5f9b65cfb296..6db53100698b 100644 --- a/packages/go_router/lib/src/builder.dart +++ b/packages/go_router/lib/src/builder.dart @@ -220,13 +220,7 @@ class RouteBuilder { pop, keyToPages[shellNavigatorKey]!, shellNavigatorKey, restorationScopeId: restorationScopeId); child = _buildStatefulNavigationShell( - shellRoute: route, - shellRouterState: state, - navigator: child as Navigator, - matchList: matchList, - pop: pop, - registry: registry, - ); + route, child as Navigator, state, matchList, pop, registry); } else { final String? restorationScopeId = (route is ShellRoute) ? route.restorationScopeId : null; @@ -269,12 +263,13 @@ class RouteBuilder { } StatefulNavigationShell _buildStatefulNavigationShell( - {required StatefulShellRoute shellRoute, - required Navigator navigator, - required GoRouterState shellRouterState, - required RouteMatchList matchList, - required VoidCallback pop, - required Map, GoRouterState> registry}) { + StatefulShellRoute shellRoute, + Navigator navigator, + GoRouterState shellRouterState, + RouteMatchList matchList, + VoidCallback pop, + Map, GoRouterState> registry, + ) { return StatefulNavigationShell( configuration: configuration, shellRoute: shellRoute, diff --git a/packages/go_router/lib/src/delegate.dart b/packages/go_router/lib/src/delegate.dart index 1e4e1bf16459..c49f8c0c4584 100644 --- a/packages/go_router/lib/src/delegate.dart +++ b/packages/go_router/lib/src/delegate.dart @@ -165,12 +165,6 @@ class GoRouterDelegate extends RouterDelegate notifyListeners(); } - /// Replaces the entire page stack. - void replaceMatchList(RouteMatchList matchList) { - _matchList = matchList; - notifyListeners(); - } - /// For internal use; visible for testing only. @visibleForTesting RouteMatchList get matches => _matchList; diff --git a/packages/go_router/lib/src/misc/stateful_navigation_shell.dart b/packages/go_router/lib/src/misc/stateful_navigation_shell.dart index e1e809325dd4..2a81b37d9c57 100644 --- a/packages/go_router/lib/src/misc/stateful_navigation_shell.dart +++ b/packages/go_router/lib/src/misc/stateful_navigation_shell.dart @@ -110,7 +110,7 @@ class StatefulNavigationShellState extends State { .processRedirection(matchList, context) .then( (RouteMatchList matchList) => - goRouter.routerDelegate.replaceMatchList(matchList), + goRouter.routerDelegate.setNewRoutePath(matchList), onError: (_) => goRouter.go(branchState.defaultLocation), ); } else { From 7b9de474c8724b08a93dc792d0609ba09914f906 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20Lo=CC=88fstrand?= Date: Fri, 4 Nov 2022 15:21:26 +0100 Subject: [PATCH 050/112] Added additional test for redirection with StatefulShellRoute. --- packages/go_router/test/go_router_test.dart | 56 +++++++++++++++++++++ 1 file changed, 56 insertions(+) diff --git a/packages/go_router/test/go_router_test.dart b/packages/go_router/test/go_router_test.dart index 392ed3a3b034..968fd9657cf2 100644 --- a/packages/go_router/test/go_router_test.dart +++ b/packages/go_router/test/go_router_test.dart @@ -2881,6 +2881,62 @@ void main() { expect(statefulWidgetKeyD.currentState?.counter, equals(0)); expect(statefulWidgetKeyE.currentState?.counter, equals(0)); }); + + testWidgets( + 'Redirects are correctly handled when switching branch in a ' + 'StatefulShellRoute', (WidgetTester tester) async { + final GlobalKey rootNavigatorKey = + GlobalKey(); + StatefulShellRouteState? routeState; + + final List routes = [ + StatefulShellRoute.rootRoutes( + builder: (BuildContext context, GoRouterState state, Widget child) { + routeState = StatefulShellRoute.of(context); + return child; + }, + routes: [ + GoRoute( + path: '/a', + builder: (BuildContext context, GoRouterState state) => + const Text('Screen A'), + ), + GoRoute( + path: '/b', + builder: (BuildContext context, GoRouterState state) => + const Text('Screen B'), + routes: [ + GoRoute( + path: 'details', + builder: (BuildContext context, GoRouterState state) => + const Text('Screen B Detail'), + ), + ], + ), + ], + ), + ]; + + await createRouter( + routes, + tester, + initialLocation: '/a', + navigatorKey: rootNavigatorKey, + redirect: (_, GoRouterState state) { + if (state.location == '/b') { + return '/b/details'; + } + return null; + }, + ); + expect(find.text('Screen A'), findsOneWidget); + expect(find.text('Screen B Detail'), findsNothing); + + routeState!.goBranch(1); + await tester.pumpAndSettle(); + expect(find.text('Screen A'), findsNothing); + expect(find.text('Screen B Detail'), findsOneWidget); + }); }); group('Imperative navigation', () { From 703815cccb0daf194cbc67443e148ad9562f25db Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20Lo=CC=88fstrand?= Date: Wed, 9 Nov 2022 22:17:03 +0100 Subject: [PATCH 051/112] Updated ShellRouteBranch to accept list of routes instead of only a single rootRoute. Removed convenience constructor StatefulShellRoute.rootRoutes. Simplified ShellRouteBranchState a bit. Extended support for automatically finding the default route for a branch. --- .../lib/stateful_nested_navigation.dart | 180 ++++---- packages/go_router/lib/src/configuration.dart | 52 ++- .../src/misc/stateful_navigation_shell.dart | 55 +-- packages/go_router/lib/src/route.dart | 183 +++----- packages/go_router/lib/src/state.dart | 18 +- packages/go_router/test/builder_test.dart | 37 +- .../go_router/test/configuration_test.dart | 364 ++++++++------- packages/go_router/test/delegate_test.dart | 41 +- packages/go_router/test/go_router_test.dart | 419 ++++++++++-------- 9 files changed, 714 insertions(+), 635 deletions(-) diff --git a/packages/go_router/example/lib/stateful_nested_navigation.dart b/packages/go_router/example/lib/stateful_nested_navigation.dart index c98c00eac88e..0037eee6facc 100644 --- a/packages/go_router/example/lib/stateful_nested_navigation.dart +++ b/packages/go_router/example/lib/stateful_nested_navigation.dart @@ -41,23 +41,25 @@ class NestedTabNavigationExampleApp extends StatelessWidget { /// The route branch for the first tab of the bottom navigation bar. ShellRouteBranch( navigatorKey: _tabANavigatorKey, - rootRoute: GoRoute( - /// The screen to display as the root in the first tab of the bottom - /// navigation bar. - path: '/a', - builder: (BuildContext context, GoRouterState state) => - const RootScreen(label: 'A', detailsPath: '/a/details'), - routes: [ - /// The details screen to display stacked on navigator of the - /// first tab. This will cover screen A but not the application - /// shell (bottom navigation bar). - GoRoute( - path: 'details', - builder: (BuildContext context, GoRouterState state) => - DetailsScreen(label: 'A', extra: state.extra), - ), - ], - ), + routes: [ + GoRoute( + /// The screen to display as the root in the first tab of the + /// bottom navigation bar. + path: '/a', + builder: (BuildContext context, GoRouterState state) => + const RootScreen(label: 'A', detailsPath: '/a/details'), + routes: [ + /// The details screen to display stacked on navigator of the + /// first tab. This will cover screen A but not the application + /// shell (bottom navigation bar). + GoRoute( + path: 'details', + builder: (BuildContext context, GoRouterState state) => + DetailsScreen(label: 'A', extra: state.extra), + ), + ], + ), + ], ), /// The route branch for the second tab of the bottom navigation bar. @@ -65,81 +67,97 @@ class NestedTabNavigationExampleApp extends StatelessWidget { /// It's not necessary to provide a navigatorKey if it isn't also /// needed elsewhere. If not provided, a default key will be used. // navigatorKey: _tabBNavigatorKey, - rootRoute: GoRoute( - /// The screen to display as the root in the second tab of the bottom - /// navigation bar. - path: '/b', - builder: (BuildContext context, GoRouterState state) => - const RootScreen( - label: 'B', - detailsPath: '/b/details/1', - secondDetailsPath: '/b/details/2'), - routes: [ - GoRoute( - path: 'details/:param', - builder: (BuildContext context, GoRouterState state) => - DetailsScreen( - label: 'B', - param: state.params['param'], - extra: state.extra), + routes: [ + GoRoute( + /// The screen to display as the root in the second tab of the + /// bottom navigation bar. + path: '/b', + builder: (BuildContext context, GoRouterState state) => + const RootScreen( + label: 'B', + detailsPath: '/b/details/1', + secondDetailsPath: '/b/details/2', ), - ], - ), + routes: [ + GoRoute( + path: 'details/:param', + builder: (BuildContext context, GoRouterState state) => + DetailsScreen( + label: 'B', + param: state.params['param'], + extra: state.extra, + ), + ), + ], + ), + ], ), /// The route branch for the third tab of the bottom navigation bar. ShellRouteBranch( - /// Since this route branch has a nested StatefulShellRoute as the - /// root route, we need to specify what the default location for the - /// branch is. - defaultLocation: '/c1', - rootRoute: StatefulShellRoute.rootRoutes( - /// This bottom tab uses a nested shell, wrapping sub routes in a - /// top TabBar. In this case, we're using the `rootRoutes` - /// convenience constructor. - routes: [ - GoRoute( - path: '/c1', - builder: (BuildContext context, GoRouterState state) => - const TabScreen(label: 'C1', detailsPath: '/c1/details'), - routes: [ + /// ShellRouteBranch will automatically use the first descendant + /// GoRoute as the default location of the branch. If another route + /// is desired, you can specify the location of it using the + /// defaultLocation parameter. + // defaultLocation: '/c2', + routes: [ + StatefulShellRoute( + /// This bottom tab uses a nested shell, wrapping sub routes in a + /// top TabBar. + branches: [ + ShellRouteBranch(routes: [ GoRoute( - path: 'details', + path: '/c1', builder: (BuildContext context, GoRouterState state) => - DetailsScreen( - label: 'C1', - extra: state.extra, - withScaffold: false), + const TabScreen( + label: 'C1', detailsPath: '/c1/details'), + routes: [ + GoRoute( + path: 'details', + builder: + (BuildContext context, GoRouterState state) => + DetailsScreen( + label: 'C1', + extra: state.extra, + withScaffold: false, + ), + ), + ], ), - ], - ), - GoRoute( - path: '/c2', - builder: (BuildContext context, GoRouterState state) => - const TabScreen(label: 'C2', detailsPath: '/c2/details'), - routes: [ + ]), + ShellRouteBranch(routes: [ GoRoute( - path: 'details', + path: '/c2', builder: (BuildContext context, GoRouterState state) => - DetailsScreen( - label: 'C2', - extra: state.extra, - withScaffold: false), + const TabScreen( + label: 'C2', detailsPath: '/c2/details'), + routes: [ + GoRoute( + path: 'details', + builder: + (BuildContext context, GoRouterState state) => + DetailsScreen( + label: 'C2', + extra: state.extra, + withScaffold: false, + ), + ), + ], ), - ], - ), - ], - builder: - (BuildContext context, GoRouterState state, Widget child) { - /// For this nested StatefulShellRoute we are using a custom - /// container (TabBarView) for the branch navigators, and thus - /// ignoring the default navigator contained passed to the - /// builder. Custom implementation can access the branch - /// navigators via the StatefulShellRouteState - /// (see TabbedRootScreen for details). - return const TabbedRootScreen(); - }, - ), + ]), + ], + builder: + (BuildContext context, GoRouterState state, Widget child) { + /// For this nested StatefulShellRoute we are using a custom + /// container (TabBarView) for the branch navigators, and thus + /// ignoring the default navigator contained passed to the + /// builder. Custom implementation can access the branch + /// navigators via the StatefulShellRouteState + /// (see TabbedRootScreen for details). + return const TabbedRootScreen(); + }, + ), + ], ), ], builder: (BuildContext context, GoRouterState state, Widget child) { diff --git a/packages/go_router/lib/src/configuration.dart b/packages/go_router/lib/src/configuration.dart index a54528d3499e..68ae6005ac21 100644 --- a/packages/go_router/lib/src/configuration.dart +++ b/packages/go_router/lib/src/configuration.dart @@ -2,6 +2,7 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'package:collection/collection.dart'; import 'package:flutter/widgets.dart'; import 'configuration.dart'; @@ -84,7 +85,7 @@ class RouteConfiguration { } else if (route is StatefulShellRoute) { for (final ShellRouteBranch branch in route.branches) { checkParentNavigatorKeys( - [branch.rootRoute], + branch.routes, >[ ...allowedKeys, branch.navigatorKey, @@ -107,17 +108,21 @@ class RouteConfiguration { if (route is StatefulShellRoute) { for (final ShellRouteBranch branch in route.branches) { if (branch.defaultLocation == null) { - continue; + // Recursively search for the first GoRoute descendant. Will + // throw assertion error if not found. + findShellRouteBranchDefaultLocation(branch); + } else { + final RouteBase defaultLocationRoute = + matcher.findMatch(branch.defaultLocation!).last.route; + final RouteBase? match = branch.routes.firstWhereOrNull( + (RouteBase e) => _debugIsDescendantOrSame( + ancestor: e, route: defaultLocationRoute)); + assert( + match != null, + 'The defaultLocation (${branch.defaultLocation}) of ' + 'ShellRouteBranch must match a descendant route of the ' + 'branch'); } - final RouteBase defaultLocationRoute = - matcher.findMatch(branch.defaultLocation!).last.route; - assert( - _debugIsDescendantOrSame( - ancestor: branch.rootRoute, - route: defaultLocationRoute), - 'The defaultLocation (${branch.defaultLocation}) of ' - 'ShellRouteBranch must match a descendant route of the ' - 'branch'); } } checkShellRouteBranchDefaultLocations(route.routes, matcher); @@ -192,9 +197,28 @@ class RouteConfiguration { .toString(); } - /// Returns the full path to the specified route. - String fullPathForRoute(RouteBase route) { - return _fullPathForRoute(route, '', routes) ?? ''; + /// Recursively traverses the routes of the provided ShellRouteBranch to find + /// the first GoRoute, from which a full path will be derived. + String findShellRouteBranchDefaultLocation(ShellRouteBranch branch) { + final GoRoute? route = _findFirstGoRoute(branch.routes); + final String? defaultLocation = + route != null ? _fullPathForRoute(route, '', routes) : null; + assert( + defaultLocation != null, + 'The default location of a ShellRouteBranch' + ' must be configured or derivable from GoRoute descendant'); + return defaultLocation!; + } + + static GoRoute? _findFirstGoRoute(List routes) { + for (final RouteBase route in routes) { + final GoRoute? match = + route is GoRoute ? route : _findFirstGoRoute(route.routes); + if (match != null) { + return match; + } + } + return null; } static String? _fullPathForRoute( diff --git a/packages/go_router/lib/src/misc/stateful_navigation_shell.dart b/packages/go_router/lib/src/misc/stateful_navigation_shell.dart index 2a81b37d9c57..0eaf2829caf2 100644 --- a/packages/go_router/lib/src/misc/stateful_navigation_shell.dart +++ b/packages/go_router/lib/src/misc/stateful_navigation_shell.dart @@ -111,13 +111,20 @@ class StatefulNavigationShellState extends State { .then( (RouteMatchList matchList) => goRouter.routerDelegate.setNewRoutePath(matchList), - onError: (_) => goRouter.go(branchState.defaultLocation), + onError: (_) => goRouter.go(_defaultBranchLocation(branchState)), ); } else { - goRouter.go(branchState.defaultLocation); + goRouter.go(_defaultBranchLocation(branchState)); } } + String _defaultBranchLocation(ShellRouteBranchState branchState) { + String? defaultLocation = branchState.routeBranch.defaultLocation; + defaultLocation ??= widget.configuration + .findShellRouteBranchDefaultLocation(branchState.routeBranch); + return defaultLocation; + } + Future _preloadBranch( ShellRouteBranchState branchState) { // Parse a RouteMatchList from the default location of the route branch and @@ -126,7 +133,8 @@ class StatefulNavigationShellState extends State { GoRouter.of(context).routeInformationParser; final Future routeMatchList = parser.parseRouteInformationWithDependencies( - RouteInformation(location: branchState.defaultLocation), context); + RouteInformation(location: _defaultBranchLocation(branchState)), + context); ShellRouteBranchState createBranchNavigator(RouteMatchList matchList) { // Find the index of the branch root route in the match list @@ -174,17 +182,29 @@ class StatefulNavigationShellState extends State { } } - String _fullPathForRoute(RouteBase route) => - widget.configuration.fullPathForRoute(route); + void _updateRouteStateFromWidget() { + final int index = _findCurrentIndex(); + + _updateRouteBranchState( + index, + _routeState.branchState[index].copy( + navigator: widget.navigator, + matchList: widget.matchList, + ), + currentIndex: index, + ); + + if (widget.shellRoute.preloadBranches && !_branchesPreloaded) { + _preloadBranches(); + _branchesPreloaded = true; + } + } @override void initState() { super.initState(); final List branchState = widget.shellRoute.branches - .map((ShellRouteBranch e) => ShellRouteBranchState( - routeBranch: e, - rootRoutePath: _fullPathForRoute(e.rootRoute), - )) + .map((ShellRouteBranch e) => ShellRouteBranchState(routeBranch: e)) .toList(); _routeState = StatefulShellRouteState( switchActiveBranch: _switchActiveBranch, @@ -206,23 +226,6 @@ class StatefulNavigationShellState extends State { _updateRouteStateFromWidget(); } - void _updateRouteStateFromWidget() { - final int index = _findCurrentIndex(); - - _updateRouteBranchState( - index, - _routeState.branchState[index].copy( - navigator: widget.navigator, - matchList: widget.matchList, - ), - currentIndex: index); - - if (widget.shellRoute.preloadBranches && !_branchesPreloaded) { - _preloadBranches(); - _branchesPreloaded = true; - } - } - @override Widget build(BuildContext context) { return InheritedStatefulNavigationShell( diff --git a/packages/go_router/lib/src/route.dart b/packages/go_router/lib/src/route.dart index b20499b6fe5a..c13ac6138a81 100644 --- a/packages/go_router/lib/src/route.dart +++ b/packages/go_router/lib/src/route.dart @@ -506,12 +506,8 @@ class ShellRoute extends ShellRouteBase { /// /// A StatefulShellRoute is created by specifying a List of [ShellRouteBranch] /// items, each representing a separate stateful branch in the route tree. -/// ShellRouteBranch provides the root route and the key [GlobalKey] for the -/// Navigator of branch, as well as an optional default location. There is also -/// a simpler shorthand way of creating a StatefulShellRoute by using a List of -/// [GoRoute]s, each representing the root route of a route branch. When using -/// this shorthand constructor, the [GoRoute.parentNavigatorKey] will be used -/// as the Navigator key. +/// ShellRouteBranch provides the root routes and the Navigator key ([GlobalKey]) +/// for the branch, as well as an optional default location. /// /// Like [ShellRoute], you can provide a [builder] and [pageBuilder] when /// creating a StatefulShellRoute. However, StatefulShellRoute differs in that @@ -581,36 +577,40 @@ class ShellRoute extends ShellRouteBase { /// /// The first branch, i.e. tab 'A' /// ShellRouteBranch( /// navigatorKey: _tabANavigatorKey, -/// rootRoute: GoRoute( -/// path: '/a', -/// builder: (BuildContext context, GoRouterState state) => -/// const RootScreen(label: 'A', detailsPath: '/a/details'), -/// routes: [ -/// /// Will cover screen A but not the bottom navigation bar -/// GoRoute( -/// path: 'details', -/// builder: (BuildContext context, GoRouterState state) => -/// const DetailsScreen(label: 'A'), -/// ), -/// ], -/// ), +/// routes: [ +/// GoRoute( +/// path: '/a', +/// builder: (BuildContext context, GoRouterState state) => +/// const RootScreen(label: 'A', detailsPath: '/a/details'), +/// routes: [ +/// /// Will cover screen A but not the bottom navigation bar +/// GoRoute( +/// path: 'details', +/// builder: (BuildContext context, GoRouterState state) => +/// const DetailsScreen(label: 'A'), +/// ), +/// ], +/// ), +/// ], /// ), /// /// The second branch, i.e. tab 'B' /// ShellRouteBranch( /// navigatorKey: _tabBNavigatorKey, -/// rootRoute: GoRoute( -/// path: '/b', -/// builder: (BuildContext context, GoRouterState state) => -/// const RootScreen(label: 'B', detailsPath: '/b/details'), -/// routes: [ -/// /// Will cover screen B but not the bottom navigation bar -/// GoRoute( -/// path: 'details', -/// builder: (BuildContext context, GoRouterState state) => -/// const DetailsScreen(label: 'B'), -/// ), -/// ], -/// ), +/// routes: [ +/// GoRoute( +/// path: '/b', +/// builder: (BuildContext context, GoRouterState state) => +/// const RootScreen(label: 'B', detailsPath: '/b/details'), +/// routes: [ +/// /// Will cover screen B but not the bottom navigation bar +/// GoRoute( +/// path: 'details', +/// builder: (BuildContext context, GoRouterState state) => +/// const DetailsScreen(label: 'B'), +/// ), +/// ], +/// ), +/// ], /// ), /// ], /// ), @@ -628,7 +628,7 @@ class ShellRoute extends ShellRouteBase { /// final GoRouter _router = GoRouter( /// initialLocation: '/a', /// routes: [ -/// StatefulShellRoute.rootRoutes( +/// StatefulShellRoute( /// builder: (BuildContext context, GoRouterState state, /// Widget navigationContainer) { /// return ScaffoldWithNavBar(body: navigationContainer); @@ -637,21 +637,25 @@ class ShellRoute extends ShellRouteBase { /// (BuildContext context, GoRouterState state, Widget statefulShell) { /// return NoTransitionPage(child: statefulShell); /// }, -/// routes: [ +/// branches: [ /// /// The first branch, i.e. root of tab 'A' -/// GoRoute( -/// parentNavigatorKey: _tabANavigatorKey, -/// path: '/a', -/// builder: (BuildContext context, GoRouterState state) => -/// const RootScreen(label: 'A', detailsPath: '/a/details'), -/// ), +/// ShellRouteBranch(routes: [ +/// GoRoute( +/// parentNavigatorKey: _tabANavigatorKey, +/// path: '/a', +/// builder: (BuildContext context, GoRouterState state) => +/// const RootScreen(label: 'A', detailsPath: '/a/details'), +/// ), +/// ]), /// /// The second branch, i.e. root of tab 'B' -/// GoRoute( -/// parentNavigatorKey: _tabBNavigatorKey, -/// path: '/b', -/// builder: (BuildContext context, GoRouterState state) => -/// const RootScreen(label: 'B', detailsPath: '/b/details/1'), -/// ), +/// ShellRouteBranch(routes: [ +/// GoRoute( +/// parentNavigatorKey: _tabBNavigatorKey, +/// path: '/b', +/// builder: (BuildContext context, GoRouterState state) => +/// const RootScreen(label: 'B', detailsPath: '/b/details/1'), +/// ), +/// ]), /// ], /// ), /// ], @@ -678,56 +682,14 @@ class StatefulShellRoute extends ShellRouteBase { /// a StatefulShellRoute. The pageBuilder however is optional, and is used /// in addition to the builder. StatefulShellRoute({ - required List branches, - required ShellRouteBuilder builder, - ShellRoutePageBuilder? pageBuilder, - bool preloadBranches = false, - }) : this._( - routes: _rootRoutes(branches), - branches: branches, - builder: builder, - pageBuilder: pageBuilder, - preloadBranches: preloadBranches, - ); - - /// Constructs a [StatefulShellRoute] from a list of [GoRoute]s, each - /// representing a root in a stateful route branch. - /// - /// This constructor provides a shorthand form of creating a - /// StatefulShellRoute from a list of GoRoutes instead of - /// [ShellRouteBranch]s. Each GoRoute provides the navigator key - /// (via [GoRoute.parentNavigatorKey]) that will be used to create the - /// separate [Navigator]s for the routes. - /// - /// Note that unlike [ShellRoute], you must always provide a builder when - /// creating a StatefulShellRoute. The pageBuilder however is optional, and is - /// used in addition to the builder. - StatefulShellRoute.rootRoutes({ - required List routes, - required ShellRouteBuilder builder, - ShellRoutePageBuilder? pageBuilder, - bool preloadBranches = false, - }) : this._( - routes: routes, - branches: routes.map((GoRoute e) { - return ShellRouteBranch( - rootRoute: e, navigatorKey: e.parentNavigatorKey); - }).toList(), - builder: builder, - preloadBranches: preloadBranches, - pageBuilder: pageBuilder, - ); - - StatefulShellRoute._({ - required super.routes, required this.branches, required super.builder, - required this.preloadBranches, super.pageBuilder, + this.preloadBranches = false, }) : assert(branches.isNotEmpty), assert(_debugUniqueNavigatorKeys(branches).length == branches.length, 'Navigator keys must be unique'), - super._() { + super._(routes: _routes(branches)) { for (int i = 0; i < routes.length; ++i) { final RouteBase route = routes[i]; if (route is GoRoute) { @@ -747,18 +709,15 @@ class StatefulShellRoute extends ShellRouteBase { /// Whether the route branches should be preloaded when navigating to this /// route for the first time. /// - /// If this is true, all the [branches] will be preloaded by navigating to the - /// [ShellRouteBranch.rootRoute], or the route matching - /// [ShellRouteBranch.defaultLocation]. + /// If this is true, all the [branches] will be preloaded by navigating to + /// their default locations (see [ShellRouteBranch] for more information). final bool preloadBranches; @override GlobalKey? navigatorKeyForSubRoute(RouteBase subRoute) { - final int routeIndex = routes.indexOf(subRoute); - if (routeIndex < 0) { - return null; - } - return branches[routeIndex].navigatorKey; + final ShellRouteBranch? branch = branches + .firstWhereOrNull((ShellRouteBranch e) => e.routes.contains(subRoute)); + return branch?.navigatorKey; } /// Gets the state for the nearest stateful shell route in the Widget tree. @@ -775,45 +734,43 @@ class StatefulShellRoute extends ShellRouteBase { Set>.from( branches.map((ShellRouteBranch e) => e.navigatorKey)); - static List _rootRoutes(List branches) => - branches.map((ShellRouteBranch e) => e.rootRoute).toList(); + static List _routes(List branches) => + branches.expand((ShellRouteBranch e) => e.routes).toList(); } /// Representation of a separate branch in a stateful navigation tree, used to /// configure [StatefulShellRoute]. /// /// The only required argument when creating a ShellRouteBranch is the -/// [rootRoute], however in some cases you may also need to specify the -/// [defaultLocation], for instance of you're using another shell route as the -/// rootRoute. A [navigatorKey] can be useful to provide in case you need to -/// use the [Navigator] created for this branch elsewhere. +/// sub-routes ([routes]), however in some cases you may also need to specify +/// the [defaultLocation], for instance of you're using another shell route as +/// direct sub-route. A [navigatorKey] can be useful to provide in case you need +/// to use the [Navigator] created for this branch elsewhere. class ShellRouteBranch { /// Constructs a [ShellRouteBranch]. ShellRouteBranch({ - required this.rootRoute, + required this.routes, GlobalKey? navigatorKey, this.defaultLocation, this.restorationScopeId, }) : navigatorKey = navigatorKey ?? GlobalKey(), - assert(rootRoute is GoRoute || defaultLocation != null, - 'Provide a defaultLocation or use a GoRoute as rootRoute'); + assert(routes.isNotEmpty); /// The [GlobalKey] to be used by the [Navigator] built for this route branch. /// /// A separate Navigator will be built for each ShellRouteBranch in a /// [StatefulShellRoute] and this key will be used to identify the Navigator. - /// The [rootRoute] and all its sub-routes will be placed o onto this Navigator + /// The [routes] and all sub-routes will be placed o onto this Navigator /// instead of the root Navigator. final GlobalKey navigatorKey; - /// The root route of the route branch. - final RouteBase rootRoute; + /// The list of child routes associated with this route branch. + final List routes; /// The default location for this route branch. /// - /// If none is specified, the location of the [rootRoute] will be used. When - /// using a [rootRoute] of a different type than [GoRoute], a default location - /// must be specified. + /// If none is specified, the first descendant [GoRoute] will be used (i.e. + /// first element in [routes], or a descendant). final String? defaultLocation; /// Restoration ID to save and restore the state of the navigator, including diff --git a/packages/go_router/lib/src/state.dart b/packages/go_router/lib/src/state.dart index dd9aa8582072..474efa179fcc 100644 --- a/packages/go_router/lib/src/state.dart +++ b/packages/go_router/lib/src/state.dart @@ -314,7 +314,7 @@ class StatefulShellRouteState { /// [StatefulShellRoute] by replacing the current navigation stack with the /// one of the route branch at the provided index. If resetLocation is true, /// the branch will be reset to its default location (see - /// [ShellRouteBranchState.defaultLocation]). + /// [ShellRouteBranch.defaultLocation]). void goBranch(int index, {bool resetLocation = false}) { _switchActiveBranch(branchState[index], resetLocation ? null : branchState[index]._matchList); @@ -350,11 +350,9 @@ class ShellRouteBranchState { /// Constructs a [ShellRouteBranchState]. const ShellRouteBranchState({ required this.routeBranch, - required String rootRoutePath, this.navigator, RouteMatchList? matchList, - }) : _matchList = matchList, - _rootRoutePath = rootRoutePath; + }) : _matchList = matchList; /// Constructs a copy of this [ShellRouteBranchState], with updated values for /// some of the fields. @@ -362,7 +360,6 @@ class ShellRouteBranchState { {Navigator? navigator, RouteMatchList? matchList}) { return ShellRouteBranchState( routeBranch: routeBranch, - rootRoutePath: _rootRoutePath, navigator: navigator ?? this.navigator, matchList: matchList ?? _matchList, ); @@ -377,16 +374,9 @@ class ShellRouteBranchState { /// navigated to at least once. final Navigator? navigator; - /// Gets the defaultLocation specified in [routeBranch] or falls back to - /// the path of the root route of the branch. - String get defaultLocation => routeBranch.defaultLocation ?? _rootRoutePath; - /// The current navigation stack for the branch. final RouteMatchList? _matchList; - /// The full path at which root route for the route branch is reachable. - final String _rootRoutePath; - @override bool operator ==(Object other) { if (identical(other, this)) { @@ -396,12 +386,10 @@ class ShellRouteBranchState { return false; } return other.routeBranch == routeBranch && - other._rootRoutePath == _rootRoutePath && other.navigator == navigator && other._matchList == _matchList; } @override - int get hashCode => - Object.hash(routeBranch, _rootRoutePath, navigator, _matchList); + int get hashCode => Object.hash(routeBranch, navigator, _matchList); } diff --git a/packages/go_router/test/builder_test.dart b/packages/go_router/test/builder_test.dart index 849a517b3385..0416a1156786 100644 --- a/packages/go_router/test/builder_test.dart +++ b/packages/go_router/test/builder_test.dart @@ -115,18 +115,17 @@ void main() { final GoRouter goRouter = GoRouter( initialLocation: '/nested', routes: [ - StatefulShellRoute.rootRoutes( - builder: - (BuildContext context, GoRouterState state, Widget child) => - child, - routes: [ - GoRoute( - parentNavigatorKey: key, - path: '/nested', - builder: (BuildContext context, GoRouterState state) { - return _DetailsScreen(); - }, - ), + StatefulShellRoute( + builder: (_, __, Widget child) => child, + branches: [ + ShellRouteBranch(navigatorKey: key, routes: [ + GoRoute( + path: '/nested', + builder: (BuildContext context, GoRouterState state) { + return _DetailsScreen(); + }, + ), + ]), ], ), ], @@ -387,12 +386,14 @@ void main() { ShellRouteBranch( navigatorKey: shellNavigatorKey, restorationScopeId: 'scope1', - rootRoute: GoRoute( - path: '/a', - builder: (BuildContext context, GoRouterState state) { - return _DetailsScreen(); - }, - ), + routes: [ + GoRoute( + path: '/a', + builder: (BuildContext context, GoRouterState state) { + return _DetailsScreen(); + }, + ), + ], ), ], ), diff --git a/packages/go_router/test/configuration_test.dart b/packages/go_router/test/configuration_test.dart index be230b3c3ba5..b81b3dd307e6 100644 --- a/packages/go_router/test/configuration_test.dart +++ b/packages/go_router/test/configuration_test.dart @@ -98,22 +98,26 @@ void main() { StatefulShellRoute(branches: [ ShellRouteBranch( navigatorKey: keyA, - rootRoute: GoRoute( - path: '/a', - builder: _mockScreenBuilder, - routes: [ - GoRoute( - path: 'details', - builder: _mockScreenBuilder, - parentNavigatorKey: keyB), - ]), + routes: [ + GoRoute( + path: '/a', + builder: _mockScreenBuilder, + routes: [ + GoRoute( + path: 'details', + builder: _mockScreenBuilder, + parentNavigatorKey: keyB), + ]), + ], ), ShellRouteBranch( navigatorKey: keyB, - rootRoute: GoRoute( - path: '/b', - builder: _mockScreenBuilder, - parentNavigatorKey: keyB), + routes: [ + GoRoute( + path: '/b', + builder: _mockScreenBuilder, + parentNavigatorKey: keyB), + ], ), ], builder: _mockShellBuilder), ], @@ -143,8 +147,9 @@ void main() { RouteConfiguration( navigatorKey: root, routes: [ - StatefulShellRoute.rootRoutes( - routes: shellRouteChildren, builder: _mockShellBuilder), + StatefulShellRoute(branches: [ + ShellRouteBranch(routes: shellRouteChildren) + ], builder: _mockShellBuilder), ], redirectLimit: 10, topRedirect: (BuildContext context, GoRouterState state) { @@ -180,9 +185,11 @@ void main() { routes: [ StatefulShellRoute(branches: [ ShellRouteBranch( - rootRoute: routeA, navigatorKey: sectionANavigatorKey), + routes: [routeA], + navigatorKey: sectionANavigatorKey), ShellRouteBranch( - rootRoute: routeB, navigatorKey: sectionBNavigatorKey), + routes: [routeB], + navigatorKey: sectionBNavigatorKey), ], builder: _mockShellBuilder), ], redirectLimit: 10, @@ -213,17 +220,21 @@ void main() { ShellRouteBranch( defaultLocation: '/x', navigatorKey: sectionANavigatorKey, - rootRoute: GoRoute( - path: '/a', - builder: _mockScreenBuilder, - ), + routes: [ + GoRoute( + path: '/a', + builder: _mockScreenBuilder, + ), + ], ), ShellRouteBranch( navigatorKey: sectionBNavigatorKey, - rootRoute: GoRoute( - path: '/b', - builder: _mockScreenBuilder, - ), + routes: [ + GoRoute( + path: '/b', + builder: _mockScreenBuilder, + ), + ], ), ], builder: _mockShellBuilder), ], @@ -255,22 +266,28 @@ void main() { ShellRouteBranch( defaultLocation: '/b', navigatorKey: sectionANavigatorKey, - rootRoute: GoRoute( - path: '/a', - builder: _mockScreenBuilder, - ), + routes: [ + GoRoute( + path: '/a', + builder: _mockScreenBuilder, + ), + ], ), ShellRouteBranch( defaultLocation: '/b', navigatorKey: sectionBNavigatorKey, - rootRoute: StatefulShellRoute(branches: [ - ShellRouteBranch( - rootRoute: GoRoute( - path: '/b', - builder: _mockScreenBuilder, + routes: [ + StatefulShellRoute(branches: [ + ShellRouteBranch( + routes: [ + GoRoute( + path: '/b', + builder: _mockScreenBuilder, + ), + ], ), - ), - ], builder: _mockShellBuilder), + ], builder: _mockShellBuilder), + ], ), ], builder: _mockShellBuilder), ], @@ -295,56 +312,82 @@ void main() { routes: [ StatefulShellRoute(branches: [ ShellRouteBranch( - rootRoute: GoRoute( - path: '/a', - builder: _mockScreenBuilder, - routes: [ - GoRoute( - path: 'detail', - builder: _mockScreenBuilder, - ), - ]), + routes: [ + GoRoute( + path: '/a', + builder: _mockScreenBuilder, + routes: [ + GoRoute( + path: 'detail', + builder: _mockScreenBuilder, + ), + ]), + ], ), ShellRouteBranch( defaultLocation: '/b/detail', - rootRoute: GoRoute( - path: '/b', - builder: _mockScreenBuilder, - routes: [ - GoRoute( - path: 'detail', - builder: _mockScreenBuilder, - ), - ]), + routes: [ + GoRoute( + path: '/b', + builder: _mockScreenBuilder, + routes: [ + GoRoute( + path: 'detail', + builder: _mockScreenBuilder, + ), + ]), + ], ), ShellRouteBranch( defaultLocation: '/c/detail', - rootRoute: StatefulShellRoute(branches: [ - ShellRouteBranch( - rootRoute: GoRoute( - path: '/c', - builder: _mockScreenBuilder, - routes: [ - GoRoute( - path: 'detail', + routes: [ + StatefulShellRoute(branches: [ + ShellRouteBranch( + routes: [ + GoRoute( + path: '/c', builder: _mockScreenBuilder, - ), - ]), - ), - ShellRouteBranch( - defaultLocation: '/d/detail', - rootRoute: GoRoute( - path: '/d', - builder: _mockScreenBuilder, - routes: [ - GoRoute( - path: 'detail', + routes: [ + GoRoute( + path: 'detail', + builder: _mockScreenBuilder, + ), + ]), + ], + ), + ShellRouteBranch( + defaultLocation: '/d/detail', + routes: [ + GoRoute( + path: '/d', builder: _mockScreenBuilder, - ), - ]), - ), - ], builder: _mockShellBuilder), + routes: [ + GoRoute( + path: 'detail', + builder: _mockScreenBuilder, + ), + ]), + ], + ), + ], builder: _mockShellBuilder), + ], ), + ShellRouteBranch(routes: [ + ShellRoute( + builder: _mockShellBuilder, + routes: [ + ShellRoute( + builder: _mockShellBuilder, + routes: [ + GoRoute( + path: '/e', + builder: _mockScreenBuilder, + ), + ], + ) + ], + ), + ]), ], builder: _mockShellBuilder), ], redirectLimit: 10, @@ -354,6 +397,86 @@ void main() { ); }); + test( + 'derives the correct defaultLocation for a ShellRouteBranch', + () { + final ShellRouteBranch branchA; + final ShellRouteBranch branchY; + final ShellRouteBranch branchB; + + final RouteConfiguration config = RouteConfiguration( + navigatorKey: GlobalKey(debugLabel: 'root'), + routes: [ + StatefulShellRoute( + builder: (_, __, Widget child) => child, + branches: [ + branchA = ShellRouteBranch(routes: [ + GoRoute( + path: '/a', + builder: _mockScreenBuilder, + routes: [ + GoRoute( + path: 'x', + builder: _mockScreenBuilder, + routes: [ + StatefulShellRoute( + builder: (_, __, Widget child) => child, + branches: [ + branchY = ShellRouteBranch(routes: [ + ShellRoute( + builder: _mockShellBuilder, + routes: [ + GoRoute( + path: 'y1', + builder: _mockScreenBuilder, + ), + GoRoute( + path: 'y2', + builder: _mockScreenBuilder, + ), + ]) + ]) + ]), + ], + ), + ], + ), + ]), + branchB = ShellRouteBranch(routes: [ + ShellRoute( + builder: _mockShellBuilder, + routes: [ + ShellRoute( + builder: _mockShellBuilder, + routes: [ + GoRoute( + path: '/b1', + builder: _mockScreenBuilder, + ), + GoRoute( + path: '/b2', + builder: _mockScreenBuilder, + ), + ], + ) + ], + ), + ]), + ], + ), + ], + redirectLimit: 10, + topRedirect: (BuildContext context, GoRouterState state) { + return null; + }, + ); + + expect('/a', config.findShellRouteBranchDefaultLocation(branchA)); + expect('/a/x/y1', config.findShellRouteBranchDefaultLocation(branchY)); + expect('/b1', config.findShellRouteBranchDefaultLocation(branchB)); + }, + ); + test( 'throws when there is a GoRoute ancestor with a different parentNavigatorKey', () { @@ -728,93 +851,6 @@ void main() { throwsAssertionError, ); }); - - test( - 'Reports correct full path for route', - () { - final GoRoute routeC1 = GoRoute( - path: 'c1', - builder: _mockScreenBuilder, - ); - final GoRoute routeY2 = - GoRoute(path: 'y2', builder: _mockScreenBuilder, routes: [ - GoRoute( - path: 'z2', - builder: _mockScreenBuilder, - ), - ]); - final GoRoute routeZ1 = GoRoute( - path: 'z1/:param', - builder: _mockScreenBuilder, - ); - final RouteConfiguration config = RouteConfiguration( - navigatorKey: GlobalKey(debugLabel: 'root'), - routes: [ - ShellRoute( - routes: [ - GoRoute( - path: '/', - builder: _mockScreenBuilder, - routes: [ - GoRoute( - path: 'a', - builder: _mockScreenBuilder, - routes: [ - ShellRoute( - routes: [ - GoRoute( - path: 'b1', - builder: _mockScreenBuilder, - routes: [ - routeC1, - ], - ), - GoRoute( - path: 'b2', - builder: _mockScreenBuilder, - routes: [ - GoRoute( - path: 'c2', - builder: _mockScreenBuilder, - ), - ], - ), - ], - ), - ], - ), - GoRoute( - path: 'x', - builder: _mockScreenBuilder, - routes: [ - ShellRoute( - routes: [ - GoRoute( - path: 'y1', - builder: _mockScreenBuilder, - routes: [routeZ1], - ), - routeY2, - ], - ), - ], - ), - ], - ), - ], - ), - ], - redirectLimit: 10, - topRedirect: (BuildContext context, GoRouterState state) { - return null; - }, - ); - - expect('/a/b1/c1', config.fullPathForRoute(routeC1)); - expect('/x/y2', config.fullPathForRoute(routeY2)); - expect('/x/y1/z1/:param', config.fullPathForRoute(routeZ1)); - }, - ); } class _MockScreen extends StatelessWidget { diff --git a/packages/go_router/test/delegate_test.dart b/packages/go_router/test/delegate_test.dart index 1c89e1168cca..bcb4e11676de 100644 --- a/packages/go_router/test/delegate_test.dart +++ b/packages/go_router/test/delegate_test.dart @@ -37,23 +37,30 @@ Future createGoRouterWithStatefulShellRoute( routes: [ GoRoute(path: '/', builder: (_, __) => const DummyStatefulWidget()), GoRoute(path: '/a', builder: (_, __) => const DummyStatefulWidget()), - StatefulShellRoute.rootRoutes(routes: [ - GoRoute( - path: '/c', - builder: (_, __) => const DummyStatefulWidget(), - routes: [ - GoRoute( - path: 'c1', builder: (_, __) => const DummyStatefulWidget()), - GoRoute( - path: 'c2', builder: (_, __) => const DummyStatefulWidget()), - ]), - GoRoute( - path: '/d', - builder: (_, __) => const DummyStatefulWidget(), - routes: [ - GoRoute( - path: 'd1', builder: (_, __) => const DummyStatefulWidget()), - ]), + StatefulShellRoute(branches: [ + ShellRouteBranch(routes: [ + GoRoute( + path: '/c', + builder: (_, __) => const DummyStatefulWidget(), + routes: [ + GoRoute( + path: 'c1', + builder: (_, __) => const DummyStatefulWidget()), + GoRoute( + path: 'c2', + builder: (_, __) => const DummyStatefulWidget()), + ]), + ]), + ShellRouteBranch(routes: [ + GoRoute( + path: '/d', + builder: (_, __) => const DummyStatefulWidget(), + routes: [ + GoRoute( + path: 'd1', + builder: (_, __) => const DummyStatefulWidget()), + ]), + ]), ], builder: (_, __, Widget child) => child), ], ); diff --git a/packages/go_router/test/go_router_test.dart b/packages/go_router/test/go_router_test.dart index 968fd9657cf2..c58df40e2c60 100644 --- a/packages/go_router/test/go_router_test.dart +++ b/packages/go_router/test/go_router_test.dart @@ -2543,40 +2543,37 @@ void main() { 'and maintains state', (WidgetTester tester) async { final GlobalKey rootNavigatorKey = GlobalKey(); - final GlobalKey sectionANavigatorKey = - GlobalKey(); - final GlobalKey sectionBNavigatorKey = - GlobalKey(); final GlobalKey statefulWidgetKey = GlobalKey(); final List routes = [ - StatefulShellRoute.rootRoutes( - builder: (BuildContext context, GoRouterState state, Widget child) => - child, - routes: [ - GoRoute( - parentNavigatorKey: sectionANavigatorKey, - path: '/a', - builder: (BuildContext context, GoRouterState state) => - const Text('Screen A'), - routes: [ - GoRoute( - path: 'detailA', - builder: (BuildContext context, GoRouterState state) => - Column(children: [ - const Text('Screen A Detail'), - DummyStatefulWidget(key: statefulWidgetKey), - ]), - ), - ], - ), - GoRoute( - parentNavigatorKey: sectionBNavigatorKey, - path: '/b', - builder: (BuildContext context, GoRouterState state) => - const Text('Screen B'), - ), + StatefulShellRoute( + builder: (_, __, Widget child) => child, + branches: [ + ShellRouteBranch(routes: [ + GoRoute( + path: '/a', + builder: (BuildContext context, GoRouterState state) => + const Text('Screen A'), + routes: [ + GoRoute( + path: 'detailA', + builder: (BuildContext context, GoRouterState state) => + Column(children: [ + const Text('Screen A Detail'), + DummyStatefulWidget(key: statefulWidgetKey), + ]), + ), + ], + ), + ]), + ShellRouteBranch(routes: [ + GoRoute( + path: '/b', + builder: (BuildContext context, GoRouterState state) => + const Text('Screen B'), + ), + ]), ], ), ]; @@ -2618,36 +2615,41 @@ void main() { GlobalKey(); final List routes = [ - StatefulShellRoute.rootRoutes( - builder: (BuildContext context, GoRouterState state, Widget child) => - child, - routes: [ - GoRoute( - parentNavigatorKey: sectionANavigatorKey, - path: '/a', - builder: (BuildContext context, GoRouterState state) => - const Text('Screen A'), - routes: [ - GoRoute( - path: 'detailA', - builder: (BuildContext context, GoRouterState state) => - const Text('Screen A Detail'), - ), - ], - ), - GoRoute( - parentNavigatorKey: sectionBNavigatorKey, - path: '/b', - builder: (BuildContext context, GoRouterState state) => - const Text('Screen B'), - routes: [ - GoRoute( - path: 'detailB', - builder: (BuildContext context, GoRouterState state) => - const Text('Screen B Detail'), - ), - ], - ), + StatefulShellRoute( + builder: (_, __, Widget child) => child, + branches: [ + ShellRouteBranch( + navigatorKey: sectionANavigatorKey, + routes: [ + GoRoute( + path: '/a', + builder: (BuildContext context, GoRouterState state) => + const Text('Screen A'), + routes: [ + GoRoute( + path: 'detailA', + builder: (BuildContext context, GoRouterState state) => + const Text('Screen A Detail'), + ), + ], + ), + ]), + ShellRouteBranch( + navigatorKey: sectionBNavigatorKey, + routes: [ + GoRoute( + path: '/b', + builder: (BuildContext context, GoRouterState state) => + const Text('Screen B'), + routes: [ + GoRoute( + path: 'detailB', + builder: (BuildContext context, GoRouterState state) => + const Text('Screen B Detail'), + ), + ], + ), + ]), ], ), ]; @@ -2681,31 +2683,29 @@ void main() { 'between branches in StatefulShellRoute', (WidgetTester tester) async { final GlobalKey rootNavigatorKey = GlobalKey(); - final GlobalKey sectionANavigatorKey = - GlobalKey(); - final GlobalKey sectionBNavigatorKey = - GlobalKey(); StatefulShellRouteState? routeState; final List routes = [ - StatefulShellRoute.rootRoutes( - builder: (BuildContext context, GoRouterState state, Widget child) { + StatefulShellRoute( + builder: (BuildContext context, _, Widget child) { routeState = StatefulShellRoute.of(context); return child; }, - routes: [ - GoRoute( - parentNavigatorKey: sectionANavigatorKey, - path: '/a', - builder: (BuildContext context, GoRouterState state) => - const Text('Screen A'), - ), - GoRoute( - parentNavigatorKey: sectionBNavigatorKey, - path: '/b', - builder: (BuildContext context, GoRouterState state) => - Text('Screen B - ${state.extra}'), - ), + branches: [ + ShellRouteBranch(routes: [ + GoRoute( + path: '/a', + builder: (BuildContext context, GoRouterState state) => + const Text('Screen A'), + ), + ]), + ShellRouteBranch(routes: [ + GoRoute( + path: '/b', + builder: (BuildContext context, GoRouterState state) => + Text('Screen B - ${state.extra}'), + ), + ]), ], ), ]; @@ -2736,10 +2736,6 @@ void main() { (WidgetTester tester) async { final GlobalKey rootNavigatorKey = GlobalKey(); - final GlobalKey sectionANavigatorKey = - GlobalKey(); - final GlobalKey sectionBNavigatorKey = - GlobalKey(); StatefulShellRouteState? routeState; final List routes = [ @@ -2748,24 +2744,26 @@ void main() { builder: (BuildContext context, GoRouterState state) => Text('Common - ${state.extra}'), ), - StatefulShellRoute.rootRoutes( + StatefulShellRoute( builder: (BuildContext context, GoRouterState state, Widget child) { routeState = StatefulShellRoute.of(context); return child; }, - routes: [ - GoRoute( - parentNavigatorKey: sectionANavigatorKey, - path: '/a', - builder: (BuildContext context, GoRouterState state) => - const Text('Screen A'), - ), - GoRoute( - parentNavigatorKey: sectionBNavigatorKey, - path: '/b', - builder: (BuildContext context, GoRouterState state) => - const Text('Screen B'), - ), + branches: [ + ShellRouteBranch(routes: [ + GoRoute( + path: '/a', + builder: (BuildContext context, GoRouterState state) => + const Text('Screen A'), + ), + ]), + ShellRouteBranch(routes: [ + GoRoute( + path: '/b', + builder: (BuildContext context, GoRouterState state) => + const Text('Screen B'), + ), + ]), ], ), ]; @@ -2812,48 +2810,58 @@ void main() { GlobalKey(); final List routes = [ - StatefulShellRoute.rootRoutes( + StatefulShellRoute( builder: (BuildContext context, GoRouterState state, Widget child) => child, - routes: [ - GoRoute( - path: '/a', - builder: (BuildContext context, GoRouterState state) => - DummyStatefulWidget(key: statefulWidgetKeyA), - ), - GoRoute( - path: '/b', - builder: (BuildContext context, GoRouterState state) => - DummyStatefulWidget(key: statefulWidgetKeyB), - ), + branches: [ + ShellRouteBranch(routes: [ + GoRoute( + path: '/a', + builder: (BuildContext context, GoRouterState state) => + DummyStatefulWidget(key: statefulWidgetKeyA), + ), + ]), + ShellRouteBranch(routes: [ + GoRoute( + path: '/b', + builder: (BuildContext context, GoRouterState state) => + DummyStatefulWidget(key: statefulWidgetKeyB), + ), + ]), ], ), - StatefulShellRoute.rootRoutes( + StatefulShellRoute( preloadBranches: true, builder: (BuildContext context, GoRouterState state, Widget child) => child, - routes: [ - GoRoute( - path: '/c', - builder: (BuildContext context, GoRouterState state) => - DummyStatefulWidget(key: statefulWidgetKeyC), - ), - GoRoute( - path: '/d', - builder: (BuildContext context, GoRouterState state) => - DummyStatefulWidget(key: statefulWidgetKeyD), - ), - GoRoute( - path: '/e', + branches: [ + ShellRouteBranch(routes: [ + GoRoute( + path: '/c', builder: (BuildContext context, GoRouterState state) => - const Text('E'), - routes: [ - GoRoute( - path: 'details', - builder: (BuildContext context, GoRouterState state) => - DummyStatefulWidget(key: statefulWidgetKeyE), - ), - ]), + DummyStatefulWidget(key: statefulWidgetKeyC), + ), + ]), + ShellRouteBranch(routes: [ + GoRoute( + path: '/d', + builder: (BuildContext context, GoRouterState state) => + DummyStatefulWidget(key: statefulWidgetKeyD), + ), + ]), + ShellRouteBranch(routes: [ + GoRoute( + path: '/e', + builder: (BuildContext context, GoRouterState state) => + const Text('E'), + routes: [ + GoRoute( + path: 'details', + builder: (BuildContext context, GoRouterState state) => + DummyStatefulWidget(key: statefulWidgetKeyE), + ), + ]), + ]), ], ), ]; @@ -2890,41 +2898,67 @@ void main() { StatefulShellRouteState? routeState; final List routes = [ - StatefulShellRoute.rootRoutes( + StatefulShellRoute( builder: (BuildContext context, GoRouterState state, Widget child) { routeState = StatefulShellRoute.of(context); return child; }, - routes: [ - GoRoute( - path: '/a', - builder: (BuildContext context, GoRouterState state) => - const Text('Screen A'), - ), - GoRoute( - path: '/b', - builder: (BuildContext context, GoRouterState state) => - const Text('Screen B'), - routes: [ - GoRoute( - path: 'details', - builder: (BuildContext context, GoRouterState state) => - const Text('Screen B Detail'), - ), - ], - ), + branches: [ + ShellRouteBranch(routes: [ + GoRoute( + path: '/a', + builder: (BuildContext context, GoRouterState state) => + const Text('Screen A'), + ), + ]), + ShellRouteBranch(routes: [ + GoRoute( + path: '/b', + builder: (BuildContext context, GoRouterState state) => + const Text('Screen B'), + routes: [ + GoRoute( + path: 'details1', + builder: (BuildContext context, GoRouterState state) => + const Text('Screen B Detail1'), + ), + GoRoute( + path: 'details2', + builder: (BuildContext context, GoRouterState state) => + const Text('Screen B Detail2'), + ), + ], + ), + ]), + ShellRouteBranch(routes: [ + GoRoute( + path: '/c', + redirect: (_, __) => '/c/main2', + ), + GoRoute( + path: '/c/main1', + builder: (BuildContext context, GoRouterState state) => + const Text('Screen C1'), + ), + GoRoute( + path: '/c/main2', + builder: (BuildContext context, GoRouterState state) => + const Text('Screen C2'), + ), + ]), ], ), ]; + String redirectDestinationBranchB = '/b/details1'; await createRouter( routes, tester, initialLocation: '/a', navigatorKey: rootNavigatorKey, redirect: (_, GoRouterState state) { - if (state.location == '/b') { - return '/b/details'; + if (state.location.startsWith('/b')) { + return redirectDestinationBranchB; } return null; }, @@ -2935,7 +2969,20 @@ void main() { routeState!.goBranch(1); await tester.pumpAndSettle(); expect(find.text('Screen A'), findsNothing); - expect(find.text('Screen B Detail'), findsOneWidget); + expect(find.text('Screen B Detail1'), findsOneWidget); + + routeState!.goBranch(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); + await tester.pumpAndSettle(); + expect(find.text('Screen A'), findsNothing); + expect(find.text('Screen B Detail2'), findsOneWidget); + expect(find.text('Screen C2'), findsNothing); }); }); @@ -3070,15 +3117,11 @@ void main() { (WidgetTester tester) async { final GlobalKey rootNavigatorKey = GlobalKey(); - final GlobalKey shellNavigatorKeyA = - GlobalKey(); - final GlobalKey shellNavigatorKeyB = - GlobalKey(); final GoRouter router = GoRouter( navigatorKey: rootNavigatorKey, initialLocation: '/a', routes: [ - StatefulShellRoute.rootRoutes( + StatefulShellRoute( builder: (BuildContext context, GoRouterState state, Widget child) { return Scaffold( @@ -3086,35 +3129,37 @@ void main() { body: child, ); }, - routes: [ - GoRoute( - path: '/a', - parentNavigatorKey: shellNavigatorKeyA, - builder: (BuildContext context, _) { - return const Scaffold( - body: Text('Screen A'), - ); - }, - ), - GoRoute( - path: '/b', - parentNavigatorKey: shellNavigatorKeyB, - builder: (BuildContext context, _) { - return const Scaffold( - body: Text('Screen B'), - ); - }, - routes: [ - GoRoute( - path: 'detail', - builder: (BuildContext context, _) { - return const Scaffold( - body: Text('Screen B detail'), - ); - }, - ), - ], - ), + branches: [ + ShellRouteBranch(routes: [ + GoRoute( + path: '/a', + builder: (BuildContext context, _) { + return const Scaffold( + body: Text('Screen A'), + ); + }, + ), + ]), + ShellRouteBranch(routes: [ + GoRoute( + path: '/b', + builder: (BuildContext context, _) { + return const Scaffold( + body: Text('Screen B'), + ); + }, + routes: [ + GoRoute( + path: 'detail', + builder: (BuildContext context, _) { + return const Scaffold( + body: Text('Screen B detail'), + ); + }, + ), + ], + ), + ]), ], ), ], From 9f889284188984c557d476f82363f4e51d5c3d7c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20Lo=CC=88fstrand?= Date: Wed, 9 Nov 2022 22:54:17 +0100 Subject: [PATCH 052/112] Added support for resetting StatefulShellRouteState. --- .../src/misc/stateful_navigation_shell.dart | 22 +++++-- packages/go_router/lib/src/state.dart | 22 +++++-- packages/go_router/test/go_router_test.dart | 66 +++++++++++++++++++ 3 files changed, 100 insertions(+), 10 deletions(-) diff --git a/packages/go_router/lib/src/misc/stateful_navigation_shell.dart b/packages/go_router/lib/src/misc/stateful_navigation_shell.dart index 0eaf2829caf2..5c86dd849269 100644 --- a/packages/go_router/lib/src/misc/stateful_navigation_shell.dart +++ b/packages/go_router/lib/src/misc/stateful_navigation_shell.dart @@ -200,20 +200,32 @@ class StatefulNavigationShellState extends State { } } - @override - void initState() { - super.initState(); + void _resetState() { + final ShellRouteBranchState currentBranchState = + _routeState.branchState[_routeState.index]; + _setupInitialStatefulShellRouteState(index: _routeState.index); + GoRouter.of(context).go(_defaultBranchLocation(currentBranchState)); + } + + void _setupInitialStatefulShellRouteState({int index = 0}) { final List branchState = widget.shellRoute.branches .map((ShellRouteBranch e) => ShellRouteBranchState(routeBranch: e)) .toList(); _routeState = StatefulShellRouteState( - switchActiveBranch: _switchActiveBranch, route: widget.shellRoute, branchState: branchState, - index: 0, + index: index, + switchActiveBranch: _switchActiveBranch, + resetState: _resetState, ); } + @override + void initState() { + super.initState(); + _setupInitialStatefulShellRouteState(); + } + @override void didUpdateWidget(covariant StatefulNavigationShell oldWidget) { super.didUpdateWidget(oldWidget); diff --git a/packages/go_router/lib/src/state.dart b/packages/go_router/lib/src/state.dart index 474efa179fcc..d2b54ab56c03 100644 --- a/packages/go_router/lib/src/state.dart +++ b/packages/go_router/lib/src/state.dart @@ -270,22 +270,25 @@ class GoRouterStateRegistry extends ChangeNotifier { class StatefulShellRouteState { /// Constructs a [StatefulShellRouteState]. const StatefulShellRouteState({ - required Function(ShellRouteBranchState, RouteMatchList?) - switchActiveBranch, required this.route, required this.branchState, required this.index, - }) : _switchActiveBranch = switchActiveBranch; + required void Function(ShellRouteBranchState, RouteMatchList?) + switchActiveBranch, + required void Function() resetState, + }) : _switchActiveBranch = switchActiveBranch, + _resetState = resetState; /// Constructs a copy of this [StatefulShellRouteState], with updated values /// for some of the fields. StatefulShellRouteState copy( {List? branchState, int? index}) { return StatefulShellRouteState( - switchActiveBranch: _switchActiveBranch, route: route, branchState: branchState ?? this.branchState, index: index ?? this.index, + switchActiveBranch: _switchActiveBranch, + resetState: _resetState, ); } @@ -299,7 +302,10 @@ class StatefulShellRouteState { /// The index of the currently active route branch. final int index; - final Function(ShellRouteBranchState, RouteMatchList?) _switchActiveBranch; + final void Function(ShellRouteBranchState, RouteMatchList?) + _switchActiveBranch; + + final void Function() _resetState; /// Gets the [Navigator]s for each of the route branches. /// @@ -320,6 +326,12 @@ class StatefulShellRouteState { resetLocation ? null : branchState[index]._matchList); } + /// Resets this StatefulShellRouteState by clearing all navigation state of + /// the branches, and returning the current branch to its default location. + void reset() { + _resetState(); + } + @override bool operator ==(Object other) { if (identical(other, this)) { diff --git a/packages/go_router/test/go_router_test.dart b/packages/go_router/test/go_router_test.dart index c58df40e2c60..8da927196897 100644 --- a/packages/go_router/test/go_router_test.dart +++ b/packages/go_router/test/go_router_test.dart @@ -2984,6 +2984,72 @@ void main() { expect(find.text('Screen B Detail2'), findsOneWidget); expect(find.text('Screen C2'), findsNothing); }); + + testWidgets('StatefulShellRoute is correctly reset', + (WidgetTester tester) async { + final GlobalKey rootNavigatorKey = + GlobalKey(); + StatefulShellRouteState? routeState; + + final List routes = [ + StatefulShellRoute( + builder: (BuildContext context, GoRouterState state, Widget child) { + routeState = StatefulShellRoute.of(context); + return child; + }, + branches: [ + ShellRouteBranch(routes: [ + GoRoute( + path: '/a', + builder: (BuildContext context, GoRouterState state) => + const Text('Screen A'), + routes: [ + GoRoute( + path: 'detail', + builder: (BuildContext context, GoRouterState state) => + const Text('Screen A Detail'), + ), + ], + ), + ]), + ShellRouteBranch(routes: [ + GoRoute( + path: '/b', + builder: (BuildContext context, GoRouterState state) => + const Text('Screen B'), + routes: [ + GoRoute( + path: 'detail', + builder: (BuildContext context, GoRouterState state) => + const Text('Screen B Detail'), + ), + ], + ), + ]), + ], + ), + ]; + + final GoRouter router = await createRouter(routes, tester, + initialLocation: '/a/detail', navigatorKey: rootNavigatorKey); + expect(find.text('Screen A'), findsNothing); + expect(find.text('Screen A Detail'), findsOneWidget); + + router.go('/b/detail'); + await tester.pumpAndSettle(); + expect(find.text('Screen B'), findsNothing); + expect(find.text('Screen B Detail'), findsOneWidget); + + routeState!.reset(); + await tester.pumpAndSettle(); + expect(find.text('Screen B'), findsOneWidget); + expect(find.text('Screen B Detail'), findsNothing); + + routeState!.goBranch(0); + await tester.pumpAndSettle(); + expect(find.text('Screen A'), findsOneWidget); + expect(find.text('Screen A Detail'), findsNothing); + }); }); group('Imperative navigation', () { From 5ca533d9cc5f3f6a4fa58662fa984dd0e2c9b6d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20Lo=CC=88fstrand?= Date: Wed, 9 Nov 2022 23:43:58 +0100 Subject: [PATCH 053/112] Renamed and changed signature of the navigator getters of StatefulShellRouteState/ShellRouteBranchState. --- .../lib/stateful_nested_navigation.dart | 12 ++++-- .../src/misc/stateful_navigation_shell.dart | 8 ++-- packages/go_router/lib/src/route.dart | 14 +++---- packages/go_router/lib/src/state.dart | 39 ++++++++++++------- 4 files changed, 43 insertions(+), 30 deletions(-) diff --git a/packages/go_router/example/lib/stateful_nested_navigation.dart b/packages/go_router/example/lib/stateful_nested_navigation.dart index 0037eee6facc..be3459e93fc8 100644 --- a/packages/go_router/example/lib/stateful_nested_navigation.dart +++ b/packages/go_router/example/lib/stateful_nested_navigation.dart @@ -165,8 +165,12 @@ class NestedTabNavigationExampleApp extends StatelessWidget { }, /// If you need to create a custom container for the branch routes, to - /// for instance setup animations, you can implement your builder - /// something like this (see _AnimatedRouteBranchContainer): + /// for instance setup custom animations, you can implement your builder + /// something like below (see _AnimatedRouteBranchContainer). Note that + /// in this case, you should not add the Widget provided in the child + /// parameter of the builder to the widget tree. Instead, you should use + /// the child widgets of each branch + /// (see StatefulShellRouteState.children). // builder: (BuildContext context, GoRouterState state, Widget child) { // return ScaffoldWithNavBar( // body: _AnimatedRouteBranchContainer(), @@ -424,7 +428,7 @@ class TabbedRootScreenTab extends StatelessWidget { // Note that we must fetch the state fresh here, since the // TabbedRootScreenTab is "cached" by the TabBarView. final StatefulShellRouteState shellState = StatefulShellRoute.of(context); - final Widget? navigator = shellState.branchState[index].navigator; + final Widget? navigator = shellState.branchState[index].child; return navigator ?? const SizedBox.expand(); } } @@ -474,7 +478,7 @@ class _AnimatedRouteBranchContainer extends StatelessWidget { final StatefulShellRouteState shellRouteState = StatefulShellRoute.of(context); return Stack( - children: shellRouteState.navigators.mapIndexed( + children: shellRouteState.children.mapIndexed( (int index, Widget? navigator) { return AnimatedScale( scale: index == shellRouteState.index ? 1 : 1.5, diff --git a/packages/go_router/lib/src/misc/stateful_navigation_shell.dart b/packages/go_router/lib/src/misc/stateful_navigation_shell.dart index 5c86dd849269..375a31eb0a5f 100644 --- a/packages/go_router/lib/src/misc/stateful_navigation_shell.dart +++ b/packages/go_router/lib/src/misc/stateful_navigation_shell.dart @@ -151,7 +151,7 @@ class StatefulNavigationShellState extends State { navigator = widget.branchNavigatorBuilder(context, navigatorMatchList, branch.navigatorKey, branch.restorationScopeId); } - return branchState.copy(navigator: navigator, matchList: matchList); + return branchState.copy(child: navigator, matchList: matchList); } return routeMatchList.then(createBranchNavigator); @@ -172,7 +172,7 @@ class StatefulNavigationShellState extends State { void _preloadBranches() { final List states = _routeState.branchState; for (int i = 0; i < states.length; i++) { - if (states[i].navigator == null) { + if (states[i].child == null) { _preloadBranch(states[i]).then((ShellRouteBranchState branchState) { setState(() { _updateRouteBranchState(i, branchState); @@ -188,7 +188,7 @@ class StatefulNavigationShellState extends State { _updateRouteBranchState( index, _routeState.branchState[index].copy( - navigator: widget.navigator, + child: widget.navigator, matchList: widget.matchList, ), currentIndex: index, @@ -275,7 +275,7 @@ class _IndexedStackedRouteBranchContainer extends StatelessWidget { Widget _buildRouteBranchContainer( BuildContext context, int index, ShellRouteBranchState routeBranch) { - final Widget? navigator = routeBranch.navigator; + final Widget? navigator = routeBranch.child; if (navigator == null) { return const SizedBox.shrink(); } diff --git a/packages/go_router/lib/src/route.dart b/packages/go_router/lib/src/route.dart index c13ac6138a81..ea54e9dbbbda 100644 --- a/packages/go_router/lib/src/route.dart +++ b/packages/go_router/lib/src/route.dart @@ -540,16 +540,16 @@ class ShellRoute extends ShellRouteBase { /// ``` /// /// Sometimes you need greater control over the layout and animations of the -/// branch Navigators. In such cases, the child argument in the builder function -/// can be ignored, and a custom implementation can instead be built using the -/// branch Navigators (see [StatefulShellRouteState.navigators]) directly. For -/// example: +/// Widgets representing the branch Navigators. In such cases, the child +/// argument in the builder function can be ignored, and a custom implementation +/// can instead be built using the child widgets of the branches +/// (see [StatefulShellRouteState.children]) directly. For example: /// /// ``` /// final StatefulShellRouteState shellState = StatefulShellRoute.of(context); -/// final int currentIndex = shellState.currentBranchIndex; -/// final List navigators = shellRouteState.branchNavigators; -/// return MyCustomShell(currentIndex, navigators); +/// final int currentIndex = shellState.index; +/// final List children = shellRouteState.children; +/// return MyCustomShell(currentIndex, children); /// ``` /// /// Below is a simple example of how a router configuration with diff --git a/packages/go_router/lib/src/state.dart b/packages/go_router/lib/src/state.dart index d2b54ab56c03..088c638648cf 100644 --- a/packages/go_router/lib/src/state.dart +++ b/packages/go_router/lib/src/state.dart @@ -307,12 +307,18 @@ class StatefulShellRouteState { final void Function() _resetState; - /// Gets the [Navigator]s for each of the route branches. + /// Gets the [Widget]s representing each of the route branches. /// - /// Note that the Navigator for a particular branch may be null if the branch - /// hasn't been visited yet. - List get navigators => - branchState.map((ShellRouteBranchState e) => e.navigator).toList(); + /// The Widget returned from this method contains the [Navigator]s of the + /// branches. Note that the Widget for a particular branch may be null if the + /// branch hasn't been visited yet. Also note that the Widgets returned by this + /// method should only be added to the widget tree if using a custom + /// branch container Widget implementation, where the child parameter in the + /// [ShellRouteBuilder] of the [StatefulShellRoute] is ignored (i.e. not added + /// to the widget tree). + /// See [ShellRouteBranchState.child]. + List get children => + branchState.map((ShellRouteBranchState e) => e.child).toList(); /// Navigate to the current location of the branch with the provided index. /// @@ -362,17 +368,16 @@ class ShellRouteBranchState { /// Constructs a [ShellRouteBranchState]. const ShellRouteBranchState({ required this.routeBranch, - this.navigator, + this.child, RouteMatchList? matchList, }) : _matchList = matchList; /// Constructs a copy of this [ShellRouteBranchState], with updated values for /// some of the fields. - ShellRouteBranchState copy( - {Navigator? navigator, RouteMatchList? matchList}) { + ShellRouteBranchState copy({Widget? child, RouteMatchList? matchList}) { return ShellRouteBranchState( routeBranch: routeBranch, - navigator: navigator ?? this.navigator, + child: child ?? this.child, matchList: matchList ?? _matchList, ); } @@ -380,11 +385,15 @@ class ShellRouteBranchState { /// The associated [ShellRouteBranch] final ShellRouteBranch routeBranch; - /// The [Navigator] for this route branch in a [StatefulShellRoute]. + /// The [Widget] representing this route branch in a [StatefulShellRoute]. /// - /// This field will typically not be set until this route tree has been - /// navigated to at least once. - final Navigator? navigator; + /// The Widget returned from this method contains the [Navigator] of the + /// branch. This field may be null until this route branch has been navigated + /// to at least once. Note that the Widget returned by this method should only + /// be added to the widget tree if using a custom branch container Widget + /// implementation, where the child parameter in the [ShellRouteBuilder] of + /// the [StatefulShellRoute] is ignored (i.e. not added to the widget tree). + final Widget? child; /// The current navigation stack for the branch. final RouteMatchList? _matchList; @@ -398,10 +407,10 @@ class ShellRouteBranchState { return false; } return other.routeBranch == routeBranch && - other.navigator == navigator && + other.child == child && other._matchList == _matchList; } @override - int get hashCode => Object.hash(routeBranch, navigator, _matchList); + int get hashCode => Object.hash(routeBranch, child, _matchList); } From 4a2eac3792bf216fd09ea53c62817a963ddc6442 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20Lo=CC=88fstrand?= Date: Fri, 18 Nov 2022 23:03:26 +0100 Subject: [PATCH 054/112] Added temporary workaround due to duplication of encodedParams of RouteMatches in redirection.dart. --- packages/go_router/lib/src/match.dart | 16 +++++ .../src/misc/stateful_navigation_shell.dart | 18 ++++- packages/go_router/test/go_router_test.dart | 69 +++++++++++++++++++ 3 files changed, 102 insertions(+), 1 deletion(-) diff --git a/packages/go_router/lib/src/match.dart b/packages/go_router/lib/src/match.dart index 622b659782ea..a345a94a72ed 100644 --- a/packages/go_router/lib/src/match.dart +++ b/packages/go_router/lib/src/match.dart @@ -33,6 +33,22 @@ class RouteMatch { return true; }()); + /// Creates a copy of this RouteMatch, with updated values for some of the + /// fields. + RouteMatch copy({Map? encodedParams}) { + return RouteMatch( + route: route, + subloc: subloc, + fullpath: fullpath, + encodedParams: encodedParams ?? this.encodedParams, + queryParams: queryParams, + queryParametersAll: queryParametersAll, + extra: extra, + error: error, + pageKey: pageKey, + ); + } + // ignore: public_member_api_docs static RouteMatch? match({ required RouteBase route, diff --git a/packages/go_router/lib/src/misc/stateful_navigation_shell.dart b/packages/go_router/lib/src/misc/stateful_navigation_shell.dart index 375a31eb0a5f..b24552737b4c 100644 --- a/packages/go_router/lib/src/misc/stateful_navigation_shell.dart +++ b/packages/go_router/lib/src/misc/stateful_navigation_shell.dart @@ -95,6 +95,21 @@ class StatefulNavigationShellState extends State { bool _branchesPreloaded = false; + // TODO(tolo): Temporary workaround due to duplication of encodedParams in redirection.dart + RouteMatchList _removeDuplicatedParams(RouteMatchList matchList) { + final Set existingParams = {}; + final List matches = []; + for (final RouteMatch match in matchList.matches) { + final Map params = { + ...match.encodedParams + }; + params.removeWhere((String key, _) => existingParams.contains(key)); + existingParams.addAll(params.keys); + matches.add(match.copy(encodedParams: params)); + } + return RouteMatchList(matches); + } + int _findCurrentIndex() { final List branchState = _routeState.branchState; final int index = branchState.indexWhere((ShellRouteBranchState e) => @@ -137,6 +152,7 @@ class StatefulNavigationShellState extends State { context); ShellRouteBranchState createBranchNavigator(RouteMatchList matchList) { + matchList = _removeDuplicatedParams(matchList); // Find the index of the branch root route in the match list final ShellRouteBranch branch = branchState.routeBranch; final int shellRouteIndex = matchList.matches @@ -189,7 +205,7 @@ class StatefulNavigationShellState extends State { index, _routeState.branchState[index].copy( child: widget.navigator, - matchList: widget.matchList, + matchList: _removeDuplicatedParams(widget.matchList), ), currentIndex: index, ); diff --git a/packages/go_router/test/go_router_test.dart b/packages/go_router/test/go_router_test.dart index 8da927196897..0880d7acae6d 100644 --- a/packages/go_router/test/go_router_test.dart +++ b/packages/go_router/test/go_router_test.dart @@ -2107,6 +2107,75 @@ void main() { expect(matches.last.decodedParams['pid'], pid); }); + testWidgets('StatefulShellRoute supports nested routes with params', + (WidgetTester tester) async { + StatefulShellRouteState? routeState; + final List routes = [ + StatefulShellRoute( + builder: (BuildContext context, _, Widget child) { + routeState = StatefulShellRoute.of(context); + return child; + }, + branches: [ + ShellRouteBranch(routes: [ + GoRoute( + path: '/a', + builder: (BuildContext context, GoRouterState state) => + const Text('Screen A'), + ), + ]), + ShellRouteBranch(routes: [ + GoRoute( + path: '/family/:fid', + builder: (BuildContext context, GoRouterState state) => + FamilyScreen(state.params['fid']!), + routes: [ + GoRoute( + path: 'person/:pid', + builder: (BuildContext context, GoRouterState state) { + final String fid = state.params['fid']!; + final String pid = state.params['pid']!; + + return PersonScreen(fid, pid); + }, + ), + ], + ), + ]), + ], + ), + ]; + + final GoRouter router = + await createRouter(routes, tester, initialLocation: '/a'); + const String fid = 'f1'; + const String pid = 'p2'; + const String loc = '/family/$fid/person/$pid'; + + router.go(loc); + await tester.pumpAndSettle(); + List matches = router.routerDelegate.matches.matches; + + expect(router.location, loc); + expect(matches, hasLength(3)); + expect(find.byType(PersonScreen), findsOneWidget); + expect(matches.last.decodedParams['fid'], fid); + expect(matches.last.decodedParams['pid'], pid); + + routeState?.goBranch(0); + await tester.pumpAndSettle(); + expect(find.text('Screen A'), findsOneWidget); + expect(find.byType(PersonScreen), findsNothing); + + routeState?.goBranch(1); + await tester.pumpAndSettle(); + expect(find.text('Screen A'), findsNothing); + expect(find.byType(PersonScreen), findsOneWidget); + matches = router.routerDelegate.matches.matches; + expect(matches.last.decodedParams['fid'], fid); + expect(matches.last.decodedParams['pid'], pid); + }); + testWidgets('goNames should allow dynamics values for queryParams', (WidgetTester tester) async { const Map queryParametersAll = >{ From a65f9df221348021a6f556340213017ff727b011 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20Lo=CC=88fstrand?= Date: Tue, 29 Nov 2022 17:19:24 +0100 Subject: [PATCH 055/112] Refactoring of StatefulShellRoute to support dynamic branches, as well as an attempt to simplify API/usage. --- packages/go_router/example/README.md | 12 +- .../lib/stateful_nested_navigation.dart | 265 ++++----- .../lib/stateful_shell_route_dynamic.dart | 280 +++++++++ packages/go_router/lib/go_router.dart | 7 +- packages/go_router/lib/src/builder.dart | 93 +-- packages/go_router/lib/src/configuration.dart | 156 +++-- packages/go_router/lib/src/delegate.dart | 58 +- packages/go_router/lib/src/matching.dart | 21 + .../src/misc/stateful_navigation_shell.dart | 174 +++--- packages/go_router/lib/src/redirection.dart | 7 +- packages/go_router/lib/src/route.dart | 283 +++++---- packages/go_router/lib/src/router.dart | 2 +- packages/go_router/lib/src/state.dart | 115 ++-- packages/go_router/lib/src/typedefs.dart | 6 + packages/go_router/test/builder_test.dart | 192 +++++- .../go_router/test/configuration_test.dart | 411 ++----------- packages/go_router/test/delegate_test.dart | 15 +- packages/go_router/test/go_router_test.dart | 552 +++++++++--------- 18 files changed, 1403 insertions(+), 1246 deletions(-) create mode 100644 packages/go_router/example/lib/stateful_shell_route_dynamic.dart diff --git a/packages/go_router/example/README.md b/packages/go_router/example/README.md index 4caff4b1193d..4649b44458b4 100644 --- a/packages/go_router/example/README.md +++ b/packages/go_router/example/README.md @@ -28,13 +28,19 @@ An example to demonstrate how to use redirect to handle a synchronous sign-in fl ## [Asynchronous Redirection](https://github.com/flutter/packages/blob/main/packages/go_router/example/lib/async_redirection.dart) `flutter run lib/async_redirection.dart` -An example to demonstrate how to use a `Shell Route` to create stateful nested navigation, with a -`BottomNavigationBar`. +An example to demonstrate how to use handle a sign-in flow with a stream authentication service. ## [Stateful Nested Navigation](https://github.com/flutter/packages/blob/main/packages/go_router/example/lib/stateful_nested_navigation.dart) `flutter run lib/stateful_nested_navigation.dart` -An example to demonstrate how to use handle a sign-in flow with a stream authentication service. +An example to demonstrate how to use a `StatefulShellRoute` to create stateful nested navigation, with a +`BottomNavigationBar`. + +## [Dynamic Stateful Nested Navigation](https://github.com/flutter/packages/blob/main/packages/go_router/example/lib/stateful_shell_route_dynamic.dart) +`flutter run lib/dynamic_stateful_shell_branches.dart` + +An example to demonstrate how to use a `StatefulShellRoute` to create stateful nested navigation, with a +`BottomNavigationBar` and a dynamic set of tabs and Navigators. ## [Books app](https://github.com/flutter/packages/blob/main/packages/go_router/example/lib/books) `flutter run lib/books/main.dart` diff --git a/packages/go_router/example/lib/stateful_nested_navigation.dart b/packages/go_router/example/lib/stateful_nested_navigation.dart index be3459e93fc8..2a1624778c5c 100644 --- a/packages/go_router/example/lib/stateful_nested_navigation.dart +++ b/packages/go_router/example/lib/stateful_nested_navigation.dart @@ -6,20 +6,30 @@ import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; -final GlobalKey _tabANavigatorKey = - GlobalKey(debugLabel: 'tabANav'); +final List _bottomNavBranches = [ + StatefulShellBranch(rootLocation: '/a', name: 'A'), + StatefulShellBranch(rootLocation: '/b', name: 'B'), + StatefulShellBranch(rootLocations: ['/c1', '/c2'], name: 'C'), + + /// To enable preloading of the root routes of the branches, pass true + /// for the parameter preload of StatefulShellBranch. + //StatefulShellBranch(rootLocations: ['/c1', '/c2'], name: 'C', preload: true), +]; + +final List _topNavBranches = [ + StatefulShellBranch(rootLocation: '/c1', name: 'C1'), + StatefulShellBranch(rootLocation: '/c2', name: 'C2'), +]; // This example demonstrates how to setup nested navigation using a // BottomNavigationBar, where each tab uses its own persistent navigator, i.e. // navigation state is maintained separately for each tab. This setup also // enables deep linking into nested pages. // -// This example demonstrates how to display routes within a ShellRoute using a -// `nestedNavigationBuilder`. Navigators for the tabs ('Section A' and -// 'Section B') are created via nested ShellRoutes. Note that no navigator will -// be created by the "top" ShellRoute. This example is similar to the ShellRoute -// example, but differs in that it is able to maintain the navigation state of -// each tab. +// This example demonstrates how to display routes within a StatefulShellRoute, +// that are places on separate navigators. The example also demonstrates how +// state is maintained when switching between different tabs (and thus branches +// and Navigators). void main() { runApp(NestedTabNavigationExampleApp()); @@ -34,132 +44,94 @@ class NestedTabNavigationExampleApp extends StatelessWidget { initialLocation: '/a', routes: [ StatefulShellRoute( - /// To enable preloading of the root routes of the branches, pass true - /// for the parameter preloadBranches. - // preloadBranches: true, - branches: [ - /// The route branch for the first tab of the bottom navigation bar. - ShellRouteBranch( - navigatorKey: _tabANavigatorKey, + routes: [ + GoRoute( + /// The screen to display as the root in the first tab of the + /// bottom navigation bar. + path: '/a', + builder: (BuildContext context, GoRouterState state) => + const RootScreen(label: 'A', detailsPath: '/a/details'), routes: [ + /// The details screen to display stacked on navigator of the + /// first tab. This will cover screen A but not the application + /// shell (bottom navigation bar). GoRoute( - /// The screen to display as the root in the first tab of the - /// bottom navigation bar. - path: '/a', + path: 'details', builder: (BuildContext context, GoRouterState state) => - const RootScreen(label: 'A', detailsPath: '/a/details'), - routes: [ - /// The details screen to display stacked on navigator of the - /// first tab. This will cover screen A but not the application - /// shell (bottom navigation bar). - GoRoute( - path: 'details', - builder: (BuildContext context, GoRouterState state) => - DetailsScreen(label: 'A', extra: state.extra), - ), - ], + DetailsScreen(label: 'A', extra: state.extra), ), ], ), - - /// The route branch for the second tab of the bottom navigation bar. - ShellRouteBranch( - /// It's not necessary to provide a navigatorKey if it isn't also - /// needed elsewhere. If not provided, a default key will be used. - // navigatorKey: _tabBNavigatorKey, + GoRoute( + /// The screen to display as the root in the second tab of the + /// bottom navigation bar. + path: '/b', + builder: (BuildContext context, GoRouterState state) => + const RootScreen( + label: 'B', + detailsPath: '/b/details/1', + secondDetailsPath: '/b/details/2', + ), routes: [ GoRoute( - /// The screen to display as the root in the second tab of the - /// bottom navigation bar. - path: '/b', + path: 'details/:param', builder: (BuildContext context, GoRouterState state) => - const RootScreen( + DetailsScreen( label: 'B', - detailsPath: '/b/details/1', - secondDetailsPath: '/b/details/2', + param: state.params['param'], + extra: state.extra, ), - routes: [ - GoRoute( - path: 'details/:param', - builder: (BuildContext context, GoRouterState state) => - DetailsScreen( - label: 'B', - param: state.params['param'], - extra: state.extra, - ), - ), - ], ), ], ), - - /// The route branch for the third tab of the bottom navigation bar. - ShellRouteBranch( - /// ShellRouteBranch will automatically use the first descendant - /// GoRoute as the default location of the branch. If another route - /// is desired, you can specify the location of it using the - /// defaultLocation parameter. - // defaultLocation: '/c2', - routes: [ - StatefulShellRoute( - /// This bottom tab uses a nested shell, wrapping sub routes in a - /// top TabBar. - branches: [ - ShellRouteBranch(routes: [ + StatefulShellRoute( + routes: [ + GoRoute( + path: '/c1', + builder: (BuildContext context, GoRouterState state) => + const TabScreen(label: 'C1', detailsPath: '/c1/details'), + routes: [ GoRoute( - path: '/c1', + path: 'details', builder: (BuildContext context, GoRouterState state) => - const TabScreen( - label: 'C1', detailsPath: '/c1/details'), - routes: [ - GoRoute( - path: 'details', - builder: - (BuildContext context, GoRouterState state) => - DetailsScreen( - label: 'C1', - extra: state.extra, - withScaffold: false, - ), - ), - ], + DetailsScreen( + label: 'C1', + extra: state.extra, + withScaffold: false, + ), ), - ]), - ShellRouteBranch(routes: [ + ], + ), + GoRoute( + path: '/c2', + builder: (BuildContext context, GoRouterState state) => + const TabScreen(label: 'C2', detailsPath: '/c2/details'), + routes: [ GoRoute( - path: '/c2', + path: 'details', builder: (BuildContext context, GoRouterState state) => - const TabScreen( - label: 'C2', detailsPath: '/c2/details'), - routes: [ - GoRoute( - path: 'details', - builder: - (BuildContext context, GoRouterState state) => - DetailsScreen( - label: 'C2', - extra: state.extra, - withScaffold: false, - ), - ), - ], + DetailsScreen( + label: 'C2', + extra: state.extra, + withScaffold: false, + ), ), - ]), - ], - builder: - (BuildContext context, GoRouterState state, Widget child) { - /// For this nested StatefulShellRoute we are using a custom - /// container (TabBarView) for the branch navigators, and thus - /// ignoring the default navigator contained passed to the - /// builder. Custom implementation can access the branch - /// navigators via the StatefulShellRouteState - /// (see TabbedRootScreen for details). - return const TabbedRootScreen(); - }, - ), - ], - ), + ], + ), + ], + branches: _topNavBranches, + builder: + (BuildContext context, GoRouterState state, Widget child) { + /// For this nested StatefulShellRoute we are using a custom + /// container (TabBarView) for the branch navigators, and thus + /// ignoring the default navigator contained passed to the + /// builder. Custom implementation can access the branch + /// navigators via the StatefulShellRouteState + /// (see TabbedRootScreen for details). + return const TabbedRootScreen(); + }), ], + branches: _bottomNavBranches, builder: (BuildContext context, GoRouterState state, Widget child) { return ScaffoldWithNavBar(body: child); }, @@ -214,6 +186,7 @@ class ScaffoldWithNavBar extends StatelessWidget { @override Widget build(BuildContext context) { final StatefulShellRouteState shellState = StatefulShellRoute.of(context); + return Scaffold( body: body, bottomNavigationBar: BottomNavigationBar( @@ -222,8 +195,9 @@ class ScaffoldWithNavBar extends StatelessWidget { BottomNavigationBarItem(icon: Icon(Icons.work), label: 'Section B'), BottomNavigationBarItem(icon: Icon(Icons.tab), label: 'Section C'), ], - currentIndex: shellState.index, - onTap: (int tappedIndex) => shellState.goBranch(tappedIndex), + currentIndex: shellState.currentIndex, + onTap: (int tappedIndex) => shellState.goBranch( + navigatorKey: _bottomNavBranches[tappedIndex].navigatorKey), ), ); } @@ -375,24 +349,30 @@ class TabbedRootScreen extends StatelessWidget { /// Constructs a TabbedRootScreen const TabbedRootScreen({Key? key}) : super(key: key); + Widget _child(StatefulShellBranchState branchState) { + // TabBarView will cache it's root widget, so we need to reevaluate + // the child (using a Builder) in case it's null. + return branchState.child != null ? branchState.child! : + Builder(builder: (BuildContext context) => + StatefulShellBranch.of(context).child ?? const SizedBox.expand()); + } + @override Widget build(BuildContext context) { final StatefulShellRouteState shellState = StatefulShellRoute.of(context); - final List children = shellState.branchState - .mapIndexed((int index, ShellRouteBranchState e) => - TabbedRootScreenTab(index: index)) - .toList(); + final List children = shellState.branchStates.map(_child).toList(); + final List tabs = children.mapIndexed((int i, _) => + Tab(text: 'Tab ${i + 1}')).toList(); return DefaultTabController( length: children.length, - initialIndex: shellState.index, + initialIndex: shellState.currentIndex, child: Scaffold( appBar: AppBar( title: const Text('Tab root'), bottom: TabBar( - tabs: children.map((TabbedRootScreenTab e) => e.tab).toList(), - onTap: (int tappedIndex) => - _onTabTap(context, shellState, tappedIndex), + tabs: tabs, + onTap: (int tappedIndex) => _onTabTap(context, tappedIndex), )), body: TabBarView( children: children, @@ -401,35 +381,9 @@ class TabbedRootScreen extends StatelessWidget { ); } - void _onTabTap( - BuildContext context, StatefulShellRouteState shellState, int index) { - shellState.goBranch(index); - } -} - -/// Widget wrapping the [Navigator] for a specific tab in [TabbedRootScreen]. -/// -/// This class is needed since [TabBarView] won't update its cached list of -/// children while in a transition between tabs. This is why we only pass the -/// index of the branch as a parameter, and fetch the state fresh in the build -/// method. -class TabbedRootScreenTab extends StatelessWidget { - /// Constructs a TabbedRootScreenTab - const TabbedRootScreenTab({Key? key, required this.index}) : super(key: key); - - /// The index of the tab - final int index; - - /// Gets the associated [Tab] object - Tab get tab => Tab(text: 'Tab ${index + 1}'); - - @override - Widget build(BuildContext context) { - // Note that we must fetch the state fresh here, since the - // TabbedRootScreenTab is "cached" by the TabBarView. - final StatefulShellRouteState shellState = StatefulShellRoute.of(context); - final Widget? navigator = shellState.branchState[index].child; - return navigator ?? const SizedBox.expand(); + void _onTabTap(BuildContext context, int index) { + StatefulShellRoute.of(context) + .goBranch(navigatorKey: _topNavBranches[index].navigatorKey); } } @@ -477,17 +431,18 @@ class _AnimatedRouteBranchContainer extends StatelessWidget { Widget build(BuildContext context) { final StatefulShellRouteState shellRouteState = StatefulShellRoute.of(context); + final int currentIndex = shellRouteState.currentIndex; return Stack( children: shellRouteState.children.mapIndexed( (int index, Widget? navigator) { return AnimatedScale( - scale: index == shellRouteState.index ? 1 : 1.5, + scale: index == currentIndex ? 1 : 1.5, duration: const Duration(milliseconds: 400), child: AnimatedOpacity( - opacity: index == shellRouteState.index ? 1 : 0, + opacity: index == currentIndex ? 1 : 0, duration: const Duration(milliseconds: 400), child: Offstage( - offstage: index != shellRouteState.index, + offstage: index != currentIndex, child: navigator ?? const SizedBox.shrink(), ), ), diff --git a/packages/go_router/example/lib/stateful_shell_route_dynamic.dart b/packages/go_router/example/lib/stateful_shell_route_dynamic.dart new file mode 100644 index 000000000000..a4881e134add --- /dev/null +++ b/packages/go_router/example/lib/stateful_shell_route_dynamic.dart @@ -0,0 +1,280 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:collection/collection.dart'; +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; + +final GlobalKey _rootNavigatorKey = +GlobalKey(debugLabel: 'root'); + +// This example demonstrates how to setup nested navigation using a +// BottomNavigationBar, using a dynamic set of tabs. Each tab uses its own +// persistent navigator, i.e. navigation state is maintained separately for each +// tab. This setup also enables deep linking into nested pages. +// +// This example demonstrates how to display routes within a StatefulShellRoute, +// that are places on separate navigators. The example also demonstrates how +// state is maintained when switching between different tabs (and thus branches +// and Navigators), as well as how to maintain a dynamic set of branches/tabs. + +void main() { + runApp(const NestedTabNavigationExampleApp()); +} + +/// An example demonstrating how to use nested navigators +class NestedTabNavigationExampleApp extends StatefulWidget { + /// Creates a NestedTabNavigationExampleApp + const NestedTabNavigationExampleApp({Key? key}) : super(key: key); + + @override + State createState() => NestedTabNavigationExampleAppState(); +} + +/// An example demonstrating how to use dynamic nested navigators +class NestedTabNavigationExampleAppState extends State { + + final List _branches = [ + StatefulShellBranch(rootLocation: '/home', name: 'Home'), + StatefulShellBranch(rootLocation: '/a/0', name: 'Dynamic 0'), + ]; + + void _addSection(StatefulShellRouteState shellRouteState) => setState(() { + if (_branches.length < 10) { + final int index = _branches.length - 1; + _branches.add(StatefulShellBranch(rootLocation: '/a/$index', name: 'Dynamic $index')); + // In situations where setState isn't possible, you can call refresh() on + // StatefulShellRouteState instead, to refresh the branches + //shellRouteState.refresh(); + } + }); + + void _removeSection(StatefulShellRouteState shellRouteState) { + if (_branches.length > 2) { + _branches.removeLast(); + // In situations where setState isn't possible, you can call refresh() on + // StatefulShellRouteState instead, to refresh the branches + shellRouteState.refresh(); + } + } + + late final GoRouter _router = GoRouter( + navigatorKey: _rootNavigatorKey, + initialLocation: '/home', + routes: [ + StatefulShellRoute( + branchBuilder: (_, __) => _branches, + routes: [ + GoRoute( + path: '/home', + builder: (BuildContext context, GoRouterState state) => RootScreen( + label: 'Home', + addSection: () => _addSection(StatefulShellRoute.of(context)), + removeSection: () => _removeSection(StatefulShellRoute.of(context)), + ), + ), + GoRoute( + /// The screen to display as the root in the first tab of the + /// bottom navigation bar. + path: '/a/:id', + builder: (BuildContext context, GoRouterState state) => + RootScreen( + label: 'A${state.params['id']}', + detailsPath: '/a/${state.params['id']}/details', + ), + routes: [ + /// The details screen to display stacked on navigator of the + /// first tab. This will cover screen A but not the application + /// shell (bottom navigation bar). + GoRoute( + path: 'details', + builder: + (BuildContext context, GoRouterState state) => + DetailsScreen(label: 'A${state.params['id']}'), + ), + ], + ), + ], + builder: + (BuildContext context, GoRouterState state, Widget child) { + return ScaffoldWithNavBar(body: child); + }, + ), + ], + ); + + @override + Widget build(BuildContext context) { + return MaterialApp.router( + title: 'Flutter Demo', + theme: ThemeData( + primarySwatch: Colors.blue, + ), + routerConfig: _router, + ); + } +} + +/// Builds the "shell" for the app by building a Scaffold with a +/// BottomNavigationBar, where [child] is placed in the body of the Scaffold. +class ScaffoldWithNavBar extends StatelessWidget { + /// Constructs an [ScaffoldWithNavBar]. + const ScaffoldWithNavBar({ + required this.body, + Key? key, + }) : super(key: key ?? const ValueKey('ScaffoldWithNavBar')); + + /// Body, i.e. the index stack + final Widget body; + + List _items(List branches) { + return branches.mapIndexed((int i, StatefulShellBranchState e) { + if (i == 0) { + return BottomNavigationBarItem(icon: const Icon(Icons.home), label: e.branch.name); + } else { + return BottomNavigationBarItem(icon: const Icon(Icons.star), label: e.branch.name); + } + }).toList(); + } + + @override + Widget build(BuildContext context) { + final StatefulShellRouteState shellState = StatefulShellRoute.of(context); + return Scaffold( + body: body, + bottomNavigationBar: BottomNavigationBar( + type: BottomNavigationBarType.fixed, + items: _items(shellState.branchStates), + currentIndex: shellState.currentIndex, + onTap: (int tappedIndex) => shellState.goBranch(index: tappedIndex), + ), + ); + } +} + +/// Widget for the root/initial pages in the bottom navigation bar. +class RootScreen extends StatelessWidget { + /// Creates a RootScreen + const RootScreen({ + required this.label, + this.detailsPath, + this.addSection, + this.removeSection, + Key? key, + }) : super(key: key); + + /// The label + final String label; + + /// The path to the detail page + final String? detailsPath; + + /// Function for adding a new branch + final VoidCallback? addSection; + /// Function for removing a branch + final VoidCallback? removeSection; + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text('Section - $label'), + ), + body: Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text('Screen $label', + style: Theme.of(context).textTheme.titleLarge), + const Padding(padding: EdgeInsets.all(4)), + if (detailsPath != null) + TextButton( + onPressed: () { + GoRouter.of(context).go(detailsPath!); + }, + child: const Text('View details'), + ), + if (addSection != null && removeSection != null) + ..._actions(context), + ], + ), + ), + ); + } + + List _actions(BuildContext context) { + return [ + const Padding(padding: EdgeInsets.all(4)), + TextButton( + onPressed: addSection, + child: const Text('Add section'), + ), + const Padding(padding: EdgeInsets.all(4)), + TextButton( + onPressed: removeSection, + child: const Text('Remove section'), + ), + ]; + } +} + +/// The details screen for either the A or B screen. +class DetailsScreen extends StatefulWidget { + /// Constructs a [DetailsScreen]. + const DetailsScreen({ + required this.label, + this.param, + Key? key, + }) : super(key: key); + + /// The label to display in the center of the screen. + final String label; + + /// Optional param + final String? param; + + @override + State createState() => DetailsScreenState(); +} + +/// The state for DetailsScreen +class DetailsScreenState extends State { + int _counter = 0; + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text('Details Screen - ${widget.label}'), + ), + body: _build(context), + ); + } + + Widget _build(BuildContext context) { + return Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text('Details for ${widget.label} - Counter: $_counter', + style: Theme.of(context).textTheme.titleLarge), + const Padding(padding: EdgeInsets.all(4)), + TextButton( + onPressed: () { + setState(() { + _counter++; + }); + }, + child: const Text('Increment counter'), + ), + const Padding(padding: EdgeInsets.all(8)), + if (widget.param != null) + Text('Parameter: ${widget.param!}', + style: Theme.of(context).textTheme.titleMedium), + const Padding(padding: EdgeInsets.all(8)), + ], + ), + ); + } +} diff --git a/packages/go_router/lib/go_router.dart b/packages/go_router/lib/go_router.dart index 13d7a9c10fb9..434a57c7bba4 100644 --- a/packages/go_router/lib/go_router.dart +++ b/packages/go_router/lib/go_router.dart @@ -12,8 +12,8 @@ export 'src/configuration.dart' GoRouterState, RouteBase, ShellRoute, - ShellRouteBranch, - ShellRouteBranchState, + StatefulShellBranch, + StatefulShellBranchState, StatefulShellRoute, StatefulShellRouteState; export 'src/misc/extensions.dart'; @@ -27,4 +27,5 @@ export 'src/typedefs.dart' GoRouterRedirect, GoRouterWidgetBuilder, ShellRouteBuilder, - ShellRoutePageBuilder; + ShellRoutePageBuilder, + StatefulShellBranchBuilder; diff --git a/packages/go_router/lib/src/builder.dart b/packages/go_router/lib/src/builder.dart index 6db53100698b..9a157fecf6a7 100644 --- a/packages/go_router/lib/src/builder.dart +++ b/packages/go_router/lib/src/builder.dart @@ -2,7 +2,6 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'package:collection/collection.dart'; import 'package:flutter/widgets.dart'; import 'configuration.dart'; @@ -126,9 +125,8 @@ class RouteBuilder { try { final Map, List>> keyToPage = , List>>{}; - final Map params = {}; _buildRecursive(context, matchList, 0, onPop, routerNeglect, keyToPage, - params, navigatorKey, registry); + navigatorKey, registry); return keyToPage[navigatorKey]!; } on _RouteBuilderError catch (e) { return >[ @@ -144,7 +142,6 @@ class RouteBuilder { VoidCallback pop, bool routerNeglect, Map, List>> keyToPages, - Map params, GlobalKey navigatorKey, Map, GoRouterState> registry, ) { @@ -159,11 +156,8 @@ class RouteBuilder { } final RouteBase route = match.route; - final Map newParams = { - ...params, - ...match.decodedParams - }; - final GoRouterState state = buildState(match, newParams); + final GoRouterState state = + buildState(match, matchList.effectiveEncodedParams(startIndex)); if (route is GoRoute) { final Page page = _buildPageForRoute(context, state, match); registry[page] = state; @@ -175,7 +169,7 @@ class RouteBuilder { keyToPages.putIfAbsent(goRouteNavKey, () => >[]).add(page); _buildRecursive(context, matchList, startIndex + 1, pop, routerNeglect, - keyToPages, newParams, navigatorKey, registry); + keyToPages, navigatorKey, registry); } else if (route is ShellRouteBase) { assert(startIndex + 1 < matchList.matches.length, 'Shell routes must always have child routes'); @@ -191,42 +185,56 @@ class RouteBuilder { // that the page for this ShellRoute is placed at the right index. final int shellPageIdx = keyToPages[parentNavigatorKey]!.length; - // Get the current sub-route of this shell route from the match list. - final RouteBase subRoute = matchList.matches[startIndex + 1].route; + void buildRecursive(GlobalKey shellNavigatorKey) { + // Add an entry for the shell route's navigator + keyToPages.putIfAbsent(shellNavigatorKey, () => >[]); - // The key to provide to the shell route's Navigator. - final GlobalKey? shellNavigatorKey = - route.navigatorKeyForSubRoute(subRoute); - assert( - shellNavigatorKey != null, - 'Shell routes must always provide a navigator key for its immediate ' - 'sub-routes'); - - // Add an entry for the shell route's navigator - keyToPages.putIfAbsent(shellNavigatorKey!, () => >[]); - - // Build the remaining pages - _buildRecursive(context, matchList, startIndex + 1, pop, routerNeglect, - keyToPages, newParams, shellNavigatorKey, registry); + // Build the remaining pages + _buildRecursive(context, matchList, startIndex + 1, pop, routerNeglect, + keyToPages, shellNavigatorKey, registry); + } // Build the Navigator and/or StatefulNavigationShell - Widget child; + Widget? child; + GlobalKey? shellNavigatorKey; if (route is StatefulShellRoute) { - final String? restorationScopeId = route.branches - .firstWhereOrNull( - (ShellRouteBranch e) => e.navigatorKey == shellNavigatorKey) - ?.restorationScopeId; + final List branches = + route.branchBuilder(context, state); + final StatefulShellBranch? branch = + route.resolveBranch(branches, state); + // Since branches may be generated dynamically, validation needs to + // occur at build time, rather than at creation of RouteConfiguration + assert(() { + return configuration.debugValidateStatefulShellBranches( + route, branches); + }()); + + assert( + branch != null, + 'Shell routes must always provide a navigator key for its immediate ' + 'sub-routes'); + // The key to provide to the shell route's Navigator. + shellNavigatorKey = branch!.navigatorKey; + buildRecursive(shellNavigatorKey); + child = _buildNavigator( pop, keyToPages[shellNavigatorKey]!, shellNavigatorKey, - restorationScopeId: restorationScopeId); + restorationScopeId: branch.restorationScopeId); + final StatefulShellBranchState shellNavigatorState = + StatefulShellBranchState( + branch: branch, child: child, matchList: matchList); child = _buildStatefulNavigationShell( - route, child as Navigator, state, matchList, pop, registry); - } else { - final String? restorationScopeId = - (route is ShellRoute) ? route.restorationScopeId : null; + route, state, branches, shellNavigatorState, pop, registry); + } else if (route is ShellRoute) { + // The key to provide to the shell route's Navigator. + shellNavigatorKey = route.navigatorKey; + buildRecursive(shellNavigatorKey); + final String? restorationScopeId = route.restorationScopeId; child = _buildNavigator( pop, keyToPages[shellNavigatorKey]!, shellNavigatorKey, restorationScopeId: restorationScopeId); + } else { + assert(false, 'Unknown route type ($route)'); } // Build the Page for this route @@ -264,9 +272,9 @@ class RouteBuilder { StatefulNavigationShell _buildStatefulNavigationShell( StatefulShellRoute shellRoute, - Navigator navigator, GoRouterState shellRouterState, - RouteMatchList matchList, + List branches, + StatefulShellBranchState currentBranchState, VoidCallback pop, Map, GoRouterState> registry, ) { @@ -274,8 +282,8 @@ class RouteBuilder { configuration: configuration, shellRoute: shellRoute, shellGoRouterState: shellRouterState, - navigator: navigator, - matchList: matchList, + branches: branches, + currentBranchState: currentBranchState, branchNavigatorBuilder: (BuildContext context, RouteMatchList navigatorMatchList, GlobalKey navigatorKey, @@ -287,6 +295,11 @@ class RouteBuilder { }); } + /// Gets the current [StatefulShellBranch] for the provided + /// [StatefulShellRoute]. + StatefulShellBranch? currentStatefulShellBranch(StatefulShellRoute route) => + route.currentBranch; + /// Helper method that builds a [GoRouterState] object for the given [match] /// and [params]. @visibleForTesting diff --git a/packages/go_router/lib/src/configuration.dart b/packages/go_router/lib/src/configuration.dart index 2300a4a7dd77..5f4589f3c433 100644 --- a/packages/go_router/lib/src/configuration.dart +++ b/packages/go_router/lib/src/configuration.dart @@ -45,7 +45,9 @@ class RouteConfiguration { // Check that each parentNavigatorKey refers to either a ShellRoute's // navigatorKey or the root navigator key. void checkParentNavigatorKeys( - List routes, List> allowedKeys) { + List routes, + List> allowedKeys, + Set> allNavigatorKeys) { for (final RouteBase route in routes) { if (route is GoRoute) { final GlobalKey? parentKey = @@ -59,6 +61,7 @@ class RouteConfiguration { " an ancestor ShellRoute's navigatorKey or GoRouter's" ' navigatorKey'); + allNavigatorKeys.add(parentKey); checkParentNavigatorKeys( route.routes, >[ @@ -66,6 +69,7 @@ class RouteConfiguration { // or keys above it can be used. ...allowedKeys.sublist(0, allowedKeys.indexOf(parentKey) + 1), ], + allNavigatorKeys, ); } else { checkParentNavigatorKeys( @@ -73,69 +77,31 @@ class RouteConfiguration { >[ ...allowedKeys, ], + allNavigatorKeys, ); } } else if (route is ShellRoute && route.navigatorKey != null) { + allNavigatorKeys.add(route.navigatorKey); checkParentNavigatorKeys( route.routes, >[ ...allowedKeys..add(route.navigatorKey) ], + allNavigatorKeys, ); } else if (route is StatefulShellRoute) { - for (final ShellRouteBranch branch in route.branches) { - checkParentNavigatorKeys( - branch.routes, - >[ - ...allowedKeys, - branch.navigatorKey, - ], - ); - } + checkParentNavigatorKeys( + route.routes, allowedKeys, allNavigatorKeys); } } } + final Set> allNavigatorKeys = + >{navigatorKey}; checkParentNavigatorKeys( - routes, >[navigatorKey]); - - // Check to see that the configured defaultLocation of ShellRouteBranches - // points to a descendant route of the route branch. - void checkShellRouteBranchDefaultLocations( - List routes, RouteMatcher matcher) { - try { - for (final RouteBase route in routes) { - if (route is StatefulShellRoute) { - for (final ShellRouteBranch branch in route.branches) { - if (branch.defaultLocation == null) { - // Recursively search for the first GoRoute descendant. Will - // throw assertion error if not found. - findShellRouteBranchDefaultLocation(branch); - } else { - final RouteBase defaultLocationRoute = - matcher.findMatch(branch.defaultLocation!).last.route; - final RouteBase? match = branch.routes.firstWhereOrNull( - (RouteBase e) => _debugIsDescendantOrSame( - ancestor: e, route: defaultLocationRoute)); - assert( - match != null, - 'The defaultLocation (${branch.defaultLocation}) of ' - 'ShellRouteBranch must match a descendant route of the ' - 'branch'); - } - } - } - checkShellRouteBranchDefaultLocations(route.routes, matcher); - } - } on MatcherError catch (e) { - assert( - false, - 'defaultLocation (${e.location}) of ShellRouteBranch must ' - 'be a valid location'); - } - } + routes, >[navigatorKey], allNavigatorKeys); - checkShellRouteBranchDefaultLocations(routes, RouteMatcher(this)); + _debugAllNavigatorKeys = allNavigatorKeys; return true; }()); @@ -155,6 +121,8 @@ class RouteConfiguration { final Map _nameToPath = {}; + late final Set>? _debugAllNavigatorKeys; + /// Looks up the url location by a [GoRoute]'s name. String namedLocation( String name, { @@ -197,48 +165,62 @@ class RouteConfiguration { .toString(); } - /// Recursively traverses the routes of the provided ShellRouteBranch to find - /// the first GoRoute, from which a full path will be derived. - String findShellRouteBranchDefaultLocation(ShellRouteBranch branch) { - final GoRoute? route = _findFirstGoRoute(branch.routes); - final String? defaultLocation = - route != null ? _fullPathForRoute(route, '', routes) : null; - assert( - defaultLocation != null, - 'The default location of a ShellRouteBranch' - ' must be configured or derivable from GoRoute descendant'); - return defaultLocation!; - } - - static GoRoute? _findFirstGoRoute(List routes) { - for (final RouteBase route in routes) { - final GoRoute? match = - route is GoRoute ? route : _findFirstGoRoute(route.routes); - if (match != null) { - return match; + /// Validates the branches of a [StatefulShellRoute]. + bool debugValidateStatefulShellBranches( + StatefulShellRoute shellRoute, List branches) { + assert(() { + final Set> uniqueBranchNavigatorKeys = + branches.map((StatefulShellBranch e) => e.navigatorKey).toSet(); + assert(uniqueBranchNavigatorKeys.length == branches.length, + 'StatefulShellRoute must not uses duplicate Navigator keys for the branches'); + + final Set uniqueDefaultLocations = + branches.map((StatefulShellBranch e) => e.defaultLocation).toSet(); + assert(uniqueDefaultLocations.length == branches.length, + 'StatefulShellRoute must not uses duplicate defaultLocations for the branches'); + + assert( + _debugAllNavigatorKeys! + .intersection(uniqueBranchNavigatorKeys) + .isEmpty, + 'StatefulShellBranch Navigator key must be unique'); + + // Check to see that the configured defaultLocation of + // StatefulShellBranches points to a descendant route of the route branch. + void checkBranchDefaultLocations( + List routes, RouteMatcher matcher) { + try { + for (final RouteBase route in routes) { + if (route is StatefulShellRoute) { + for (final StatefulShellBranch branch in branches) { + final String defaultLocation = branch.defaultLocation; + final RouteBase defaultLocationRoute = + matcher.findMatch(defaultLocation).last.route; + final RouteBase? match = shellRoute.routes.firstWhereOrNull( + (RouteBase e) => _debugIsDescendantOrSame( + ancestor: e, route: defaultLocationRoute)); + assert( + match != null, + 'The defaultLocation (${branch.defaultLocation}) of ' + 'StatefulShellBranch must match a descendant route of the ' + 'StatefulShellRoute'); + } + } + checkBranchDefaultLocations(route.routes, matcher); + } + } on MatcherError catch (e) { + assert( + false, + 'defaultLocation (${e.location}) of StatefulShellBranch must ' + 'be a valid location'); + } } - } - return null; - } - static String? _fullPathForRoute( - RouteBase targetRoute, String parentFullpath, List routes) { - for (final RouteBase route in routes) { - final String fullPath = (route is GoRoute) - ? concatenatePaths(parentFullpath, route.path) - : parentFullpath; + checkBranchDefaultLocations(routes, RouteMatcher(this)); - if (route == targetRoute) { - return fullPath; - } else { - final String? subRoutePath = - _fullPathForRoute(targetRoute, fullPath, route.routes); - if (subRoutePath != null) { - return subRoutePath; - } - } - } - return null; + return true; + }()); + return true; } /// Tests if a route is a descendant of, or same as, an ancestor route. diff --git a/packages/go_router/lib/src/delegate.dart b/packages/go_router/lib/src/delegate.dart index 084925549466..8eadc7922a0e 100644 --- a/packages/go_router/lib/src/delegate.dart +++ b/packages/go_router/lib/src/delegate.dart @@ -60,40 +60,37 @@ class GoRouterDelegate extends RouterDelegate final Map _pushCounts = {}; final RouteConfiguration _configuration; + GlobalKey? _navigatorKeyForRouteMatch(int matchListIndex) { + final RouteMatch match = _matchList.matches[matchListIndex]; + final RouteBase route = match.route; + if (route is GoRoute && route.parentNavigatorKey != null) { + return route.parentNavigatorKey; + } else if (route is ShellRoute) { + return route.navigatorKey; + } else if (route is StatefulShellRoute) { + return builder.currentStatefulShellBranch(route)!.navigatorKey; + } + return null; + } + @override Future popRoute() async { // Iterate backwards through the RouteMatchList until seeing a GoRoute with // a non-null parentNavigatorKey or a ShellRoute with a non-null // parentNavigatorKey and pop from that Navigator instead of the root. final int matchCount = _matchList.matches.length; - late RouteBase subRoute; for (int i = matchCount - 1; i >= 0; i -= 1) { - final RouteMatch match = _matchList.matches[i]; - final RouteBase route = match.route; - - if (route is GoRoute && route.parentNavigatorKey != null) { - final bool didPop = - await route.parentNavigatorKey!.currentState!.maybePop(); - - // Continue if didPop was false. - if (didPop) { - return didPop; - } - } else if (route is ShellRouteBase) { - // For shell routes, find the navigator key that should be used for the - // child route in the current match list - final GlobalKey? navigatorKey = - route.navigatorKeyForSubRoute(subRoute); + final GlobalKey? navigatorKey = + _navigatorKeyForRouteMatch(i); - final bool didPop = - await navigatorKey?.currentState!.maybePop() ?? false; + if (navigatorKey != null) { + final bool didPop = await navigatorKey.currentState!.maybePop(); // Continue if didPop was false. if (didPop) { return didPop; } } - subRoute = route; } // Use the root navigator if no ShellRoute Navigators were found and didn't @@ -133,31 +130,18 @@ class GoRouterDelegate extends RouterDelegate bool canPop() { // Loop through navigators in reverse and call canPop() final int matchCount = _matchList.matches.length; - late RouteBase subRoute; for (int i = matchCount - 1; i >= 0; i -= 1) { - final RouteMatch match = _matchList.matches[i]; - final RouteBase route = match.route; - if (route is GoRoute && route.parentNavigatorKey != null) { - final bool canPop = route.parentNavigatorKey!.currentState!.canPop(); - - // Continue if canPop is false. - if (canPop) { - return canPop; - } - } else if (route is ShellRouteBase) { - // For shell routes, find the navigator key that should be used for the - // child route in the current match list - final GlobalKey? navigatorKey = - route.navigatorKeyForSubRoute(subRoute); + final GlobalKey? navigatorKey = + _navigatorKeyForRouteMatch(i); - final bool canPop = navigatorKey?.currentState?.canPop() ?? false; + if (navigatorKey != null) { + final bool canPop = navigatorKey.currentState!.canPop(); // Continue if canPop is false. if (canPop) { return canPop; } } - subRoute = route; } return navigatorKey.currentState?.canPop() ?? false; } diff --git a/packages/go_router/lib/src/matching.dart b/packages/go_router/lib/src/matching.dart index c817cf5b426c..e332301d7284 100644 --- a/packages/go_router/lib/src/matching.dart +++ b/packages/go_router/lib/src/matching.dart @@ -94,6 +94,27 @@ class RouteMatchList { /// Returns the error that this match intends to display. Exception? get error => matches.first.error; + + /// A copy of the last route match, updated to include all the effective path + /// parameters of the current matches. + RouteMatch get lastWithEffectiveEncodedParams { + final Map params = matches.fold( + {}, + (Map p, RouteMatch e) => + {...p, ...e.encodedParams}); + return last.copy(encodedParams: params); + } + + /// Returns the encoded path parameters for all matches up to and including the + /// provided index in this RouteMatchList. + Map effectiveEncodedParams(int matchIndex) { + assert(matchIndex >= 0); + final Map params = {}; + for (int i = 0; i <= matchIndex; i++) { + params.addAll(matches[i].decodedParams); + } + return params; + } } /// An error that occurred during matching. diff --git a/packages/go_router/lib/src/misc/stateful_navigation_shell.dart b/packages/go_router/lib/src/misc/stateful_navigation_shell.dart index b24552737b4c..ad43cd0679ad 100644 --- a/packages/go_router/lib/src/misc/stateful_navigation_shell.dart +++ b/packages/go_router/lib/src/misc/stateful_navigation_shell.dart @@ -47,7 +47,7 @@ typedef ShellRouteBranchNavigatorBuilder = Navigator Function( /// associated StatefulShellRoute, and exposes the state (represented by /// [StatefulShellRouteState]) to its child widgets with the help of the /// InheritedWidget [InheritedStatefulNavigationShell]. The state for each route -/// branch is represented by [ShellRouteBranchState] and can be accessed via the +/// branch is represented by [StatefulShellBranchState] and can be accessed via the /// StatefulShellRouteState. /// /// By default, this widget creates a container for the branch route Navigators, @@ -61,8 +61,8 @@ class StatefulNavigationShell extends StatefulWidget { required this.configuration, required this.shellRoute, required this.shellGoRouterState, - required this.navigator, - required this.matchList, + required this.branches, + required this.currentBranchState, required this.branchNavigatorBuilder, super.key, }); @@ -76,11 +76,11 @@ class StatefulNavigationShell extends StatefulWidget { /// The [GoRouterState] for the navigation shell. final GoRouterState shellGoRouterState; - /// The navigator for the currently active route branch - final Navigator navigator; + /// The currently active set of [StatefulShellBranchState]s. + final List branches; - /// The RouteMatchList for the current location - final RouteMatchList matchList; + /// The [StatefulShellBranchState] for the current location. + final StatefulShellBranchState currentBranchState; /// Builder for route branch navigators (used for preloading). final ShellRouteBranchNavigatorBuilder branchNavigatorBuilder; @@ -93,32 +93,15 @@ class StatefulNavigationShell extends StatefulWidget { class StatefulNavigationShellState extends State { late StatefulShellRouteState _routeState; - bool _branchesPreloaded = false; - - // TODO(tolo): Temporary workaround due to duplication of encodedParams in redirection.dart - RouteMatchList _removeDuplicatedParams(RouteMatchList matchList) { - final Set existingParams = {}; - final List matches = []; - for (final RouteMatch match in matchList.matches) { - final Map params = { - ...match.encodedParams - }; - params.removeWhere((String key, _) => existingParams.contains(key)); - existingParams.addAll(params.keys); - matches.add(match.copy(encodedParams: params)); - } - return RouteMatchList(matches); - } - int _findCurrentIndex() { - final List branchState = _routeState.branchState; - final int index = branchState.indexWhere((ShellRouteBranchState e) => - e.routeBranch.navigatorKey == widget.navigator.key); - return index < 0 ? 0 : index; + final int index = widget.branches.indexWhere( + (StatefulShellBranch e) => e == widget.currentBranchState.branch); + assert(index >= 0); + return index; } void _switchActiveBranch( - ShellRouteBranchState branchState, RouteMatchList? matchList) { + StatefulShellBranchState navigatorState, RouteMatchList? matchList) { final GoRouter goRouter = GoRouter.of(context); if (matchList != null && matchList.isNotEmpty) { goRouter.routeInformationParser @@ -126,35 +109,27 @@ class StatefulNavigationShellState extends State { .then( (RouteMatchList matchList) => goRouter.routerDelegate.setNewRoutePath(matchList), - onError: (_) => goRouter.go(_defaultBranchLocation(branchState)), + onError: (_) => goRouter.go(navigatorState.branch.defaultLocation), ); } else { - goRouter.go(_defaultBranchLocation(branchState)); + goRouter.go(navigatorState.branch.defaultLocation); } } - String _defaultBranchLocation(ShellRouteBranchState branchState) { - String? defaultLocation = branchState.routeBranch.defaultLocation; - defaultLocation ??= widget.configuration - .findShellRouteBranchDefaultLocation(branchState.routeBranch); - return defaultLocation; - } - - Future _preloadBranch( - ShellRouteBranchState branchState) { + Future _preloadBranch( + StatefulShellBranchState navigatorState) { // Parse a RouteMatchList from the default location of the route branch and // handle any redirects final GoRouteInformationParser parser = GoRouter.of(context).routeInformationParser; final Future routeMatchList = parser.parseRouteInformationWithDependencies( - RouteInformation(location: _defaultBranchLocation(branchState)), + RouteInformation(location: navigatorState.branch.defaultLocation), context); - ShellRouteBranchState createBranchNavigator(RouteMatchList matchList) { - matchList = _removeDuplicatedParams(matchList); + StatefulShellBranchState createBranchNavigator(RouteMatchList matchList) { // Find the index of the branch root route in the match list - final ShellRouteBranch branch = branchState.routeBranch; + final StatefulShellBranch branch = navigatorState.branch; final int shellRouteIndex = matchList.matches .indexWhere((RouteMatch e) => e.route == widget.shellRoute); // Keep only the routes from and below the root route in the match list and @@ -167,31 +142,45 @@ class StatefulNavigationShellState extends State { navigator = widget.branchNavigatorBuilder(context, navigatorMatchList, branch.navigatorKey, branch.restorationScopeId); } - return branchState.copy(child: navigator, matchList: matchList); + return navigatorState.copy(child: navigator, matchList: matchList); } return routeMatchList.then(createBranchNavigator); } - void _updateRouteBranchState(int index, ShellRouteBranchState branchState, + void _updateRouteBranchState(StatefulShellBranchState navigatorState, {int? currentIndex}) { - final List branchStates = - _routeState.branchState.toList(); - branchStates[index] = branchState; + final List branches = widget.branches; + final List existingStates = + _routeState.branchStates; + final List newStates = + []; + + for (final StatefulShellBranch branch in branches) { + if (branch.navigatorKey == navigatorState.navigatorKey) { + newStates.add(navigatorState); + } else { + newStates.add(existingStates.firstWhereOrNull( + (StatefulShellBranchState e) => e.branch == branch) ?? + StatefulShellBranchState(branch: branch)); + } + } _routeState = _routeState.copy( - branchState: branchStates, - index: currentIndex, + branchStates: newStates, + currentIndex: currentIndex, ); } void _preloadBranches() { - final List states = _routeState.branchState; - for (int i = 0; i < states.length; i++) { - if (states[i].child == null) { - _preloadBranch(states[i]).then((ShellRouteBranchState branchState) { + final List states = _routeState.branchStates; + for (StatefulShellBranchState state in states) { + if (state.branch.preload && state.child == null) { + // Set a placeholder widget as child to prevent repeated preloading + state = state.copy(child: const SizedBox.shrink()); + _preloadBranch(state).then((StatefulShellBranchState navigatorState) { setState(() { - _updateRouteBranchState(i, branchState); + _updateRouteBranchState(navigatorState); }); }); } @@ -200,37 +189,30 @@ class StatefulNavigationShellState extends State { void _updateRouteStateFromWidget() { final int index = _findCurrentIndex(); - _updateRouteBranchState( - index, - _routeState.branchState[index].copy( - child: widget.navigator, - matchList: _removeDuplicatedParams(widget.matchList), - ), + widget.currentBranchState, currentIndex: index, ); - if (widget.shellRoute.preloadBranches && !_branchesPreloaded) { - _preloadBranches(); - _branchesPreloaded = true; - } + _preloadBranches(); } void _resetState() { - final ShellRouteBranchState currentBranchState = - _routeState.branchState[_routeState.index]; - _setupInitialStatefulShellRouteState(index: _routeState.index); - GoRouter.of(context).go(_defaultBranchLocation(currentBranchState)); + final StatefulShellBranchState navigatorState = + _routeState.currentBranchState; + _setupInitialStatefulShellRouteState(); + GoRouter.of(context).go(navigatorState.branch.defaultLocation); } - void _setupInitialStatefulShellRouteState({int index = 0}) { - final List branchState = widget.shellRoute.branches - .map((ShellRouteBranch e) => ShellRouteBranchState(routeBranch: e)) + void _setupInitialStatefulShellRouteState() { + final List states = widget.branches + .map((StatefulShellBranch e) => StatefulShellBranchState(branch: e)) .toList(); + _routeState = StatefulShellRouteState( route: widget.shellRoute, - branchState: branchState, - index: index, + branchStates: states, + currentIndex: 0, switchActiveBranch: _switchActiveBranch, resetState: _resetState, ); @@ -281,21 +263,24 @@ class _IndexedStackedRouteBranchContainer extends StatelessWidget { @override Widget build(BuildContext context) { - final List children = routeState.branchState - .mapIndexed((int index, ShellRouteBranchState item) => - _buildRouteBranchContainer(context, index, item)) + final StatefulShellBranchState currentState = routeState.currentBranchState; + final List states = routeState.branchStates; + final List children = states + .map((StatefulShellBranchState e) => + _buildRouteBranchContainer(context, e == currentState, e)) .toList(); - return IndexedStack(index: routeState.index, children: children); + final int currentIndex = + states.indexWhere((StatefulShellBranchState e) => e == currentState); + return IndexedStack(index: currentIndex, children: children); } - Widget _buildRouteBranchContainer( - BuildContext context, int index, ShellRouteBranchState routeBranch) { - final Widget? navigator = routeBranch.child; + Widget _buildRouteBranchContainer(BuildContext context, bool isActive, + StatefulShellBranchState navigatorState) { + final Widget? navigator = navigatorState.child; if (navigator == null) { return const SizedBox.shrink(); } - final bool isActive = index == routeState.index; return Offstage( offstage: !isActive, child: TickerMode( @@ -305,3 +290,26 @@ class _IndexedStackedRouteBranchContainer extends StatelessWidget { ); } } + +/// StatefulShellRoute extension that provides support for resolving the +/// current StatefulShellBranch. +extension StatefulShellBranchResolver on StatefulShellRoute { + static final Expando _shellBranchCache = + Expando(); + + /// The current StatefulShellBranch, previously resolved using [resolveBranch]. + StatefulShellBranch? get currentBranch => _shellBranchCache[this]; + + /// Resolves the current StatefulShellBranch, given the provided GoRouterState. + StatefulShellBranch? resolveBranch( + List branches, GoRouterState state) { + final StatefulShellBranch? branch = branches + .firstWhereOrNull((StatefulShellBranch e) => e.isBranchFor(state)); + _shellBranchCache[this] = branch; + return branch; + } +} + +extension _StatefulShellBranchStateHelper on StatefulShellBranchState { + GlobalKey get navigatorKey => branch.navigatorKey; +} diff --git a/packages/go_router/lib/src/redirection.dart b/packages/go_router/lib/src/redirection.dart index 996aa346083d..aec1ec08cf71 100644 --- a/packages/go_router/lib/src/redirection.dart +++ b/packages/go_router/lib/src/redirection.dart @@ -52,14 +52,13 @@ FutureOr redirect( // Merge new params to keep params from previously matched paths, e.g. // /users/:userId/book/:bookId provides userId and bookId to bookgit /:bookId - Map previouslyMatchedParams = {}; + final Map previouslyMatchedParams = {}; for (final RouteMatch match in prevMatchList.matches) { assert( !previouslyMatchedParams.keys.any(match.encodedParams.containsKey), 'Duplicated parameter names', ); - match.encodedParams.addAll(previouslyMatchedParams); - previouslyMatchedParams = match.encodedParams; + previouslyMatchedParams.addAll(match.encodedParams); } FutureOr processRouteLevelRedirect( String? routeRedirectLocation) { @@ -154,7 +153,7 @@ FutureOr _getRouteLevelRedirect( path: route.path, fullpath: match.fullpath, extra: match.extra, - params: match.decodedParams, + params: matchList.effectiveEncodedParams(currentCheckIndex), queryParams: match.queryParams, queryParametersAll: match.queryParametersAll, ), diff --git a/packages/go_router/lib/src/route.dart b/packages/go_router/lib/src/route.dart index de8f639c0fce..4d03ee154e3a 100644 --- a/packages/go_router/lib/src/route.dart +++ b/packages/go_router/lib/src/route.dart @@ -361,10 +361,6 @@ abstract class ShellRouteBase extends RouteBase { /// matching sub-routes. Typically, a shell route builds its shell around this /// Widget. final ShellRoutePageBuilder? pageBuilder; - - /// 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); } /// A route that displays a UI shell around the matching child route. @@ -489,14 +485,6 @@ class ShellRoute extends ShellRouteBase { /// Restoration ID to save and restore the state of the navigator, including /// its history. final String? restorationScopeId; - - @override - GlobalKey? navigatorKeyForSubRoute(RouteBase subRoute) { - if (routes.contains(subRoute)) { - return navigatorKey; - } - return null; - } } /// A route that displays a UI shell with separate [Navigator]s for its @@ -505,15 +493,18 @@ class ShellRoute extends ShellRouteBase { /// Similar to [ShellRoute], this route class places its sub-route on a /// different Navigator than the root Navigator. However, this route class /// differs in that it creates separate Navigators for each of its nested -/// route branches (route trees), making it possible to build an app with -/// stateful nested navigation. This is convenient when for instance +/// branches (i.e. parallel navigation trees), making it possible to build an +/// app with stateful nested navigation. This is convenient when for instance /// implementing a UI with a [BottomNavigationBar], with a persistent navigation /// state for each tab. /// -/// A StatefulShellRoute is created by specifying a List of [ShellRouteBranch] -/// items, each representing a separate stateful branch in the route tree. -/// ShellRouteBranch provides the root routes and the Navigator key ([GlobalKey]) -/// for the branch, as well as an optional default location. +/// A StatefulShellRoute is created by providing a List of [StatefulShellBranch] +/// items, each representing a separate stateful branch in the route tree. The +/// branches can be provided either statically, by passing a list of branches in +/// the constructor, or dynamically by instead providing a [branchBuilder]. +/// StatefulShellBranch defines the root location(s) of the branch, as well as +/// the Navigator key ([GlobalKey]) for the Navigator associated with the +/// branch. /// /// Like [ShellRoute], you can provide a [builder] and [pageBuilder] when /// creating a StatefulShellRoute. However, StatefulShellRoute differs in that @@ -536,12 +527,13 @@ class ShellRoute extends ShellRouteBase { /// of the route branches etc. It is also with the help this state object you /// can change the active branch, i.e. restore the navigation stack of another /// branch. This is accomplished using the method -/// [StatefulShellRouteState.goBranch]. For example: +/// [StatefulShellRouteState.goBranch], and providing either a Navigator key, +/// branch name or branch index. For example: /// /// ``` /// void _onBottomNavigationBarItemTapped(BuildContext context, int index) { /// final StatefulShellRouteState shellState = StatefulShellRoute.of(context); -/// shellState.goBranch(index); +/// shellState.goBranch(index: index); /// } /// ``` /// @@ -553,7 +545,7 @@ class ShellRoute extends ShellRouteBase { /// /// ``` /// final StatefulShellRouteState shellState = StatefulShellRoute.of(context); -/// final int currentIndex = shellState.index; +/// final int currentIndex = shellState.currentIndex; /// final List children = shellRouteState.children; /// return MyCustomShell(currentIndex, children); /// ``` @@ -566,59 +558,49 @@ class ShellRoute extends ShellRouteBase { /// of the builder function. /// /// ``` -/// final GlobalKey _tabANavigatorKey = -/// GlobalKey(debugLabel: 'tabANavigator'); -/// final GlobalKey _tabBNavigatorKey = -/// GlobalKey(debugLabel: 'tabBNavigator'); -///^ /// final GoRouter _router = GoRouter( /// initialLocation: '/a', /// routes: [ /// StatefulShellRoute( -/// builder: (BuildContext context, GoRouterState state, -/// Widget navigatorContainer) { -/// return ScaffoldWithNavBar(body: navigatorContainer); -/// }, -/// branches: [ -/// /// The first branch, i.e. tab 'A' -/// ShellRouteBranch( -/// navigatorKey: _tabANavigatorKey, +/// routes: [ +/// GoRoute( +/// /// The screen to display as the root in the first tab of the +/// /// bottom navigation bar. +/// path: '/a', +/// builder: (BuildContext context, GoRouterState state) => +/// const RootScreen(label: 'A', detailsPath: '/a/details'), /// routes: [ +/// /// Will cover screen A but not the bottom navigation bar /// GoRoute( -/// path: '/a', +/// path: 'details', /// builder: (BuildContext context, GoRouterState state) => -/// const RootScreen(label: 'A', detailsPath: '/a/details'), -/// routes: [ -/// /// Will cover screen A but not the bottom navigation bar -/// GoRoute( -/// path: 'details', -/// builder: (BuildContext context, GoRouterState state) => -/// const DetailsScreen(label: 'A'), -/// ), -/// ], +/// const DetailsScreen(label: 'A'), /// ), /// ], /// ), -/// /// The second branch, i.e. tab 'B' -/// ShellRouteBranch( -/// navigatorKey: _tabBNavigatorKey, +/// GoRoute( +/// /// The screen to display as the root in the second tab of the +/// /// bottom navigation bar. +/// path: '/b', +/// builder: (BuildContext context, GoRouterState state) => +/// const RootScreen(label: 'B', detailsPath: '/b/details'), /// routes: [ +/// /// Will cover screen B but not the bottom navigation bar /// GoRoute( -/// path: '/b', +/// path: 'details', /// builder: (BuildContext context, GoRouterState state) => -/// const RootScreen(label: 'B', detailsPath: '/b/details'), -/// routes: [ -/// /// Will cover screen B but not the bottom navigation bar -/// GoRoute( -/// path: 'details', -/// builder: (BuildContext context, GoRouterState state) => -/// const DetailsScreen(label: 'B'), -/// ), -/// ], +/// const DetailsScreen(label: 'B'), /// ), /// ], /// ), /// ], +/// branches: [ +/// StatefulShellBranch(rootLocation: '/a'), +/// StatefulShellBranch(rootLocation: '/b'), +/// ], +/// builder: (BuildContext context, GoRouterState state, Widget child) { +/// return ScaffoldWithNavBar(body: child); +/// }, /// ), /// ], /// ); @@ -635,34 +617,34 @@ class ShellRoute extends ShellRouteBase { /// initialLocation: '/a', /// routes: [ /// StatefulShellRoute( -/// builder: (BuildContext context, GoRouterState state, -/// Widget navigationContainer) { -/// return ScaffoldWithNavBar(body: navigationContainer); -/// }, -/// pageBuilder: -/// (BuildContext context, GoRouterState state, Widget statefulShell) { -/// return NoTransitionPage(child: statefulShell); -/// }, -/// branches: [ -/// /// The first branch, i.e. root of tab 'A' -/// ShellRouteBranch(routes: [ -/// GoRoute( -/// parentNavigatorKey: _tabANavigatorKey, -/// path: '/a', -/// builder: (BuildContext context, GoRouterState state) => +/// routes: [ +/// GoRoute( +/// /// The screen to display as the root in the first tab of the +/// /// bottom navigation bar. +/// path: '/a', +/// builder: (BuildContext context, GoRouterState state) => /// const RootScreen(label: 'A', detailsPath: '/a/details'), -/// ), -/// ]), -/// /// The second branch, i.e. root of tab 'B' -/// ShellRouteBranch(routes: [ -/// GoRoute( -/// parentNavigatorKey: _tabBNavigatorKey, -/// path: '/b', -/// builder: (BuildContext context, GoRouterState state) => -/// const RootScreen(label: 'B', detailsPath: '/b/details/1'), -/// ), -/// ]), +/// ), +/// GoRoute( +/// /// The screen to display as the root in the second tab of the +/// /// bottom navigation bar. +/// path: '/b', +/// builder: (BuildContext context, GoRouterState state) => +/// const RootScreen(label: 'B', detailsPath: '/b/details'), +/// ), +/// ], +/// /// To enable a dynamic set of StatefulShellBranches (and thus +/// /// Navigators), use 'branchBuilder' instead of 'branches'. +/// branchBuilder: (BuildContext context, GoRouterState state) => +/// [ +/// StatefulShellBranch(rootLocation: '/a'), +/// StatefulShellBranch(rootLocation: '/b'), /// ], +/// builder: (BuildContext context, GoRouterState state, Widget child) => +/// ScaffoldWithNavBar(body: child), +/// pageBuilder: +/// (BuildContext context, GoRouterState state, Widget statefulShell) => +/// NoTransitionPage(child: statefulShell), /// ), /// ], /// ); @@ -678,53 +660,47 @@ class ShellRoute extends ShellRouteBase { /// /// See [Stateful Nested Navigation](https://github.com/flutter/packages/blob/main/packages/go_router/example/lib/stateful_nested_navigation.dart) /// for a complete runnable example using StatefulShellRoute. +/// For an example of the use of dynamic branches, see +/// [Dynamic Stateful Nested Navigation](https://github.com/flutter/packages/blob/main/packages/go_router/example/lib/dynamic_stateful_shell_branches.dart). class StatefulShellRoute extends ShellRouteBase { - /// Constructs a [StatefulShellRoute] from a list of [ShellRouteBranch], each + /// Constructs a [StatefulShellRoute] from a list of [StatefulShellBranch], each /// representing a root in a stateful route branch. /// /// A separate [Navigator] will be created for each of the branches, using - /// the navigator key specified in [ShellRouteBranch]. Note that unlike + /// the navigator key specified in [StatefulShellBranch]. Note that unlike /// [ShellRoute], you must always provide a builder when creating /// a StatefulShellRoute. The pageBuilder however is optional, and is used /// in addition to the builder. StatefulShellRoute({ - required this.branches, + required super.routes, required super.builder, + StatefulShellBranchBuilder? branchBuilder, + List? branches, super.pageBuilder, - this.preloadBranches = false, - }) : assert(branches.isNotEmpty), - assert(_debugUniqueNavigatorKeys(branches).length == branches.length, - 'Navigator keys must be unique'), - super._(routes: _routes(branches)) { + }) : assert(branchBuilder != null || branches != null), + branchBuilder = branchBuilder ?? _builderFromBranches(branches!), + super._() { for (int i = 0; i < routes.length; ++i) { final RouteBase route = routes[i]; if (route is GoRoute) { - assert(route.parentNavigatorKey == null || - route.parentNavigatorKey == branches[i].navigatorKey); + assert(route.parentNavigatorKey == null); } } } - /// Representations of the different stateful route branches that this - /// shell route will manage. - /// - /// Each branch identifies the [Navigator] to be used (via the navigatorKey) - /// and the route that will be used as the root of the route branch. - final List branches; + static StatefulShellBranchBuilder _builderFromBranches( + List branches) { + return (_, __) => branches; + } - /// Whether the route branches should be preloaded when navigating to this - /// route for the first time. + /// The navigation branch builder for this shell route. /// - /// If this is true, all the [branches] will be preloaded by navigating to - /// their default locations (see [ShellRouteBranch] for more information). - final bool preloadBranches; - - @override - GlobalKey? navigatorKeyForSubRoute(RouteBase subRoute) { - final ShellRouteBranch? branch = branches - .firstWhereOrNull((ShellRouteBranch e) => e.routes.contains(subRoute)); - return branch?.navigatorKey; - } + /// This builder is used to provide the currently active StatefulShellBranches + /// at any point in time. Each branch uses a separate [Navigator], identified + /// by [StatefulShellBranch.navigatorKey]. + /// identifies the [Navigator] to be used + /// (via the navigatorKey) + final StatefulShellBranchBuilder branchBuilder; /// Gets the state for the nearest stateful shell route in the Widget tree. static StatefulShellRouteState of(BuildContext context) { @@ -734,52 +710,73 @@ class StatefulShellRoute extends ShellRouteBase { 'No InheritedStatefulNavigationShell found in context'); return inherited!.routeState; } - - static Set> _debugUniqueNavigatorKeys( - List branches) => - Set>.from( - branches.map((ShellRouteBranch e) => e.navigatorKey)); - - static List _routes(List branches) => - branches.expand((ShellRouteBranch e) => e.routes).toList(); } -/// Representation of a separate branch in a stateful navigation tree, used to -/// configure [StatefulShellRoute]. +/// Representation of a separate navigation branch in a [StatefulShellRoute]. /// -/// The only required argument when creating a ShellRouteBranch is the -/// sub-routes ([routes]), however in some cases you may also need to specify -/// the [defaultLocation], for instance of you're using another shell route as -/// direct sub-route. A [navigatorKey] can be useful to provide in case you need +/// The only required argument is the rootLocation (or [rootLocations]), which +/// identify the [defaultLocation] to be used when loading the branch for the +/// first time (for instance when switching branch using the goBranch method in +/// [StatefulShellBranchState]). The rootLocations also identify the valid root +/// locations for a particular StatefulShellBranch, and thus on which Navigator +/// those routes should be placed on. +/// +/// A [navigatorKey] is optional, but can be useful to provide in case you need /// to use the [Navigator] created for this branch elsewhere. -class ShellRouteBranch { - /// Constructs a [ShellRouteBranch]. - ShellRouteBranch({ - required this.routes, +class StatefulShellBranch { + /// Constructs a [StatefulShellBranch]. + StatefulShellBranch({ GlobalKey? navigatorKey, - this.defaultLocation, + List? rootLocations, + String? rootLocation, + this.name, this.restorationScopeId, - }) : navigatorKey = navigatorKey ?? GlobalKey(), - assert(routes.isNotEmpty); - - /// The [GlobalKey] to be used by the [Navigator] built for this route branch. + this.preload = false, + }) : assert(rootLocation != null || (rootLocations?.isNotEmpty ?? false)), + rootLocations = rootLocations ?? [rootLocation!], + navigatorKey = navigatorKey ?? + GlobalKey( + debugLabel: name != null ? 'Branch-$name' : null); + + /// The [GlobalKey] to be used by the [Navigator] built for this branch. /// - /// A separate Navigator will be built for each ShellRouteBranch in a + /// A separate Navigator will be built for each StatefulShellBranch in a /// [StatefulShellRoute] and this key will be used to identify the Navigator. - /// The [routes] and all sub-routes will be placed o onto this Navigator - /// instead of the root Navigator. + /// The routes associated with this branch will be placed o onto that + /// Navigator instead of the root Navigator. final GlobalKey navigatorKey; - /// The list of child routes associated with this route branch. - final List routes; + /// The valid root locations for this branch. + final List rootLocations; + + /// An optional name for this branch. + final String? name; - /// The default location for this route branch. + /// Whether this route branch should be preloaded when the associated + /// [StatefulShellRoute] is visited for the first time. /// - /// If none is specified, the first descendant [GoRoute] will be used (i.e. - /// first element in [routes], or a descendant). - final String? defaultLocation; + /// If this is true, this branch will be preloaded by navigating to + /// the root location (first entry in [rootLocations]). + final bool preload; /// Restoration ID to save and restore the state of the navigator, including /// its history. final String? restorationScopeId; + + /// Returns the default location for this branch (by default the first + /// entry in [rootLocations]). + String get defaultLocation => rootLocations.first; + + /// Checks if this branch is intended to be used for the provided + /// GoRouterState. + bool isBranchFor(GoRouterState state) { + final String? match = rootLocations + .firstWhereOrNull((String e) => state.location.startsWith(e)); + return match != null; + } + + /// Gets the state for the current branch of the nearest stateful shell route + /// in the Widget tree. + static StatefulShellBranchState of(BuildContext context) => + StatefulShellRoute.of(context).currentBranchState; } diff --git a/packages/go_router/lib/src/router.dart b/packages/go_router/lib/src/router.dart index fded5fa6c077..fcf4c00f1031 100644 --- a/packages/go_router/lib/src/router.dart +++ b/packages/go_router/lib/src/router.dart @@ -207,7 +207,7 @@ class GoRouter extends ChangeNotifier implements RouterConfig { _routerDelegate.navigatorKey.currentContext!, ) .then((RouteMatchList matches) { - _routerDelegate.push(matches.last); + _routerDelegate.push(matches.lastWithEffectiveEncodedParams); }); } diff --git a/packages/go_router/lib/src/state.dart b/packages/go_router/lib/src/state.dart index 088c638648cf..91a7b767a7f1 100644 --- a/packages/go_router/lib/src/state.dart +++ b/packages/go_router/lib/src/state.dart @@ -2,6 +2,7 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'package:collection/collection.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/widgets.dart'; @@ -271,9 +272,9 @@ class StatefulShellRouteState { /// Constructs a [StatefulShellRouteState]. const StatefulShellRouteState({ required this.route, - required this.branchState, - required this.index, - required void Function(ShellRouteBranchState, RouteMatchList?) + required this.branchStates, + required this.currentIndex, + required void Function(StatefulShellBranchState, RouteMatchList?) switchActiveBranch, required void Function() resetState, }) : _switchActiveBranch = switchActiveBranch, @@ -282,11 +283,11 @@ class StatefulShellRouteState { /// Constructs a copy of this [StatefulShellRouteState], with updated values /// for some of the fields. StatefulShellRouteState copy( - {List? branchState, int? index}) { + {List? branchStates, int? currentIndex}) { return StatefulShellRouteState( route: route, - branchState: branchState ?? this.branchState, - index: index ?? this.index, + branchStates: branchStates ?? this.branchStates, + currentIndex: currentIndex ?? this.currentIndex, switchActiveBranch: _switchActiveBranch, resetState: _resetState, ); @@ -297,17 +298,27 @@ class StatefulShellRouteState { /// The state for all separate route branches associated with a /// [StatefulShellRoute]. - final List branchState; + final List branchStates; - /// The index of the currently active route branch. - final int index; + /// The state associated with the current [ShellNavigator]. + StatefulShellBranchState get currentBranchState => branchStates[currentIndex]; - final void Function(ShellRouteBranchState, RouteMatchList?) + /// The index of the currently active [ShellNavigator]. + /// + /// Corresponds to the index of the ShellNavigator in the List returned from + /// shellNavigatorBuilder of [StatefulShellRoute]. + final int currentIndex; + + /// The Navigator key of the current navigator. + GlobalKey get currentNavigatorKey => + currentBranchState.branch.navigatorKey; + + final void Function(StatefulShellBranchState, RouteMatchList?) _switchActiveBranch; final void Function() _resetState; - /// Gets the [Widget]s representing each of the route branches. + /// Gets the [Widget]s representing each of the shell branches. /// /// The Widget returned from this method contains the [Navigator]s of the /// branches. Note that the Widget for a particular branch may be null if the @@ -316,20 +327,48 @@ class StatefulShellRouteState { /// branch container Widget implementation, where the child parameter in the /// [ShellRouteBuilder] of the [StatefulShellRoute] is ignored (i.e. not added /// to the widget tree). - /// See [ShellRouteBranchState.child]. + /// See [StatefulShellBranchState.child]. List get children => - branchState.map((ShellRouteBranchState e) => e.child).toList(); + branchStates.map((StatefulShellBranchState e) => e.child).toList(); - /// Navigate to the current location of the branch with the provided index. + /// Navigate to the current location of the shell navigator with the provided + /// Navigator key, name or index. /// /// This method will switch the currently active [Navigator] for the /// [StatefulShellRoute] by replacing the current navigation stack with the - /// one of the route branch at the provided index. If resetLocation is true, - /// the branch will be reset to its default location (see - /// [ShellRouteBranch.defaultLocation]). - void goBranch(int index, {bool resetLocation = false}) { - _switchActiveBranch(branchState[index], - resetLocation ? null : branchState[index]._matchList); + /// one of the route branch identified by the provided Navigator key, name or + /// index. If resetLocation is true, the branch will be reset to its default + /// location (see [StatefulShellBranch.defaultLocation]). + void goBranch({ + GlobalKey? navigatorKey, + String? name, + int? index, + bool resetLocation = false, + }) { + assert(navigatorKey != null || name != null || index != null); + assert([navigatorKey, name, index].whereNotNull().length == 1); + + final StatefulShellBranchState state; + if (navigatorKey != null) { + state = branchStates.firstWhere((StatefulShellBranchState e) => + e.branch.navigatorKey == navigatorKey); + assert(state != null, + 'Unable to find ShellNavigator with key $navigatorKey'); + } else if (name != null) { + state = branchStates + .firstWhere((StatefulShellBranchState e) => e.branch.name == name); + assert(state != null, 'Unable to find ShellNavigator with name "$name"'); + } else { + state = branchStates[index!]; + } + + _switchActiveBranch(state, resetLocation ? null : state._matchList); + } + + /// Refreshes this StatefulShellRouteState by rebuilding the state for the + /// current location. + void refresh() { + _switchActiveBranch(currentBranchState, currentBranchState._matchList); } /// Resets this StatefulShellRouteState by clearing all navigation state of @@ -347,43 +386,43 @@ class StatefulShellRouteState { return false; } return other.route == route && - listEquals(other.branchState, branchState) && - other.index == index; + listEquals(other.branchStates, branchStates) && + other.currentIndex == currentIndex; } @override - int get hashCode => Object.hash(route, branchState, index); + int get hashCode => Object.hash(route, currentIndex, currentIndex); } /// The snapshot of the current state for a particular route branch -/// ([ShellRouteBranch]) in a [StatefulShellRoute]. +/// ([StatefulShellBranch]) in a [StatefulShellRoute]. /// /// Note that this an immutable class, that represents the snapshot of the state -/// of a ShellRouteBranchState at a given point in time. Therefore, instances of +/// of a StatefulShellBranchState at a given point in time. Therefore, instances of /// this object should not be stored, but instead fetched fresh when needed, /// via the [StatefulShellRouteState] returned by the method /// [StatefulShellRoute.of]. @immutable -class ShellRouteBranchState { - /// Constructs a [ShellRouteBranchState]. - const ShellRouteBranchState({ - required this.routeBranch, +class StatefulShellBranchState { + /// Constructs a [StatefulShellBranchState]. + const StatefulShellBranchState({ + required this.branch, this.child, RouteMatchList? matchList, }) : _matchList = matchList; - /// Constructs a copy of this [ShellRouteBranchState], with updated values for + /// Constructs a copy of this [StatefulShellBranchState], with updated values for /// some of the fields. - ShellRouteBranchState copy({Widget? child, RouteMatchList? matchList}) { - return ShellRouteBranchState( - routeBranch: routeBranch, + StatefulShellBranchState copy({Widget? child, RouteMatchList? matchList}) { + return StatefulShellBranchState( + branch: branch, child: child ?? this.child, matchList: matchList ?? _matchList, ); } - /// The associated [ShellRouteBranch] - final ShellRouteBranch routeBranch; + /// The associated [StatefulShellBranch] + final StatefulShellBranch branch; /// The [Widget] representing this route branch in a [StatefulShellRoute]. /// @@ -403,14 +442,14 @@ class ShellRouteBranchState { if (identical(other, this)) { return true; } - if (other is! ShellRouteBranchState) { + if (other is! StatefulShellBranchState) { return false; } - return other.routeBranch == routeBranch && + return other.branch == branch && other.child == child && other._matchList == _matchList; } @override - int get hashCode => Object.hash(routeBranch, child, _matchList); + int get hashCode => Object.hash(branch, child, _matchList); } diff --git a/packages/go_router/lib/src/typedefs.dart b/packages/go_router/lib/src/typedefs.dart index 29269945b419..e7f1b2923513 100644 --- a/packages/go_router/lib/src/typedefs.dart +++ b/packages/go_router/lib/src/typedefs.dart @@ -34,6 +34,12 @@ typedef ShellRoutePageBuilder = Page Function( Widget child, ); +/// The branch builder for a [StatefulShellRoute]. +typedef StatefulShellBranchBuilder = List Function( + BuildContext context, + GoRouterState state, +); + /// The signature of the navigatorBuilder callback. typedef GoRouterNavigatorBuilder = Widget Function( BuildContext context, diff --git a/packages/go_router/test/builder_test.dart b/packages/go_router/test/builder_test.dart index 0416a1156786..4ada83c4e12d 100644 --- a/packages/go_router/test/builder_test.dart +++ b/packages/go_router/test/builder_test.dart @@ -112,34 +112,177 @@ void main() { testWidgets('Builds StatefulShellRoute', (WidgetTester tester) async { final GlobalKey key = GlobalKey(debugLabel: 'key'); - final GoRouter goRouter = GoRouter( - initialLocation: '/nested', + final RouteConfiguration config = RouteConfiguration( routes: [ StatefulShellRoute( builder: (_, __, Widget child) => child, - branches: [ - ShellRouteBranch(navigatorKey: key, routes: [ - GoRoute( - path: '/nested', - builder: (BuildContext context, GoRouterState state) { - return _DetailsScreen(); - }, - ), - ]), + branches: [ + StatefulShellBranch(rootLocation: '/nested', navigatorKey: key), + ], + routes: [ + GoRoute( + path: '/nested', + builder: (BuildContext context, GoRouterState state) { + return _DetailsScreen(); + }, + ), ], ), ], + redirectLimit: 10, + topRedirect: (_, __) => null, navigatorKey: GlobalKey(), ); - await tester.pumpWidget(MaterialApp.router( - routerConfig: goRouter, - )); + final RouteMatchList matches = RouteMatchList([ + _createRouteMatch(config.routes.first, '/nested'), + _createRouteMatch(config.routes.first.routes.first, '/nested'), + ]); + + await tester.pumpWidget( + _BuilderTestWidget( + routeConfiguration: config, + matches: matches, + ), + ); expect(find.byType(_DetailsScreen), findsOneWidget); expect(find.byKey(key), findsOneWidget); }); + testWidgets( + 'throws when a branch of a StatefulShellRoute has an incorrect ' + 'defaultLocation', (WidgetTester tester) async { + final RouteConfiguration config = RouteConfiguration( + routes: [ + StatefulShellRoute( + routes: [ + GoRoute( + path: '/a', + builder: (_, __) => _DetailsScreen(), + ), + GoRoute( + path: '/b', + builder: (_, __) => _DetailsScreen(), + ), + ], + builder: (_, __, Widget child) { + return _HomeScreen(child: child); + }, + branches: [ + StatefulShellBranch(rootLocation: '/x'), + StatefulShellBranch(rootLocation: '/b'), + ]), + ], + redirectLimit: 10, + topRedirect: (_, __) => null, + navigatorKey: GlobalKey(), + ); + + final RouteMatchList matches = RouteMatchList([ + _createRouteMatch(config.routes.first, '/b'), + _createRouteMatch(config.routes.first.routes.first, '/b'), + ]); + + await tester.pumpWidget( + _BuilderTestWidget( + routeConfiguration: config, + matches: matches, + ), + ); + + expect(tester.takeException(), isAssertionError); + }); + + testWidgets( + 'throws when a branch of a StatefulShellRoute has duplicate ' + 'defaultLocation', (WidgetTester tester) async { + final RouteConfiguration config = RouteConfiguration( + routes: [ + StatefulShellRoute( + routes: [ + GoRoute( + path: '/a', + builder: (_, __) => _DetailsScreen(), + ), + GoRoute( + path: '/b', + builder: (_, __) => _DetailsScreen(), + ), + ], + builder: (_, __, Widget child) { + return _HomeScreen(child: child); + }, + branches: [ + StatefulShellBranch(rootLocation: '/a'), + StatefulShellBranch(rootLocations: ['/a', '/b']), + ]), + ], + redirectLimit: 10, + topRedirect: (_, __) => null, + navigatorKey: GlobalKey(), + ); + + final RouteMatchList matches = RouteMatchList([ + _createRouteMatch(config.routes.first, '/b'), + _createRouteMatch(config.routes.first.routes.first, '/b'), + ]); + + await tester.pumpWidget( + _BuilderTestWidget( + routeConfiguration: config, + matches: matches, + ), + ); + + expect(tester.takeException(), isAssertionError); + }); + + testWidgets('throws when StatefulShellRoute has duplicate navigator keys', + (WidgetTester tester) async { + final GlobalKey keyA = + GlobalKey(debugLabel: 'A'); + final RouteConfiguration config = RouteConfiguration( + routes: [ + StatefulShellRoute( + routes: [ + GoRoute( + path: '/a', + builder: (_, __) => _DetailsScreen(), + ), + GoRoute( + path: '/b', + builder: (_, __) => _DetailsScreen(), + ), + ], + builder: (_, __, Widget child) { + return _HomeScreen(child: child); + }, + branches: [ + StatefulShellBranch(rootLocation: '/a', navigatorKey: keyA), + StatefulShellBranch(rootLocation: '/b', navigatorKey: keyA), + ]), + ], + redirectLimit: 10, + topRedirect: (_, __) => null, + navigatorKey: GlobalKey(), + ); + + final RouteMatchList matches = RouteMatchList([ + _createRouteMatch(config.routes.first, '/b'), + _createRouteMatch(config.routes.first.routes.first, '/b'), + ]); + + await tester.pumpWidget( + _BuilderTestWidget( + routeConfiguration: config, + matches: matches, + ), + ); + + expect(tester.takeException(), isAssertionError); + }); + testWidgets('Uses the correct navigatorKey', (WidgetTester tester) async { final GlobalKey rootNavigatorKey = GlobalKey(); @@ -382,18 +525,19 @@ void main() { builder: (BuildContext context, GoRouterState state, Widget child) { return _HomeScreen(child: child); }, - branches: [ - ShellRouteBranch( + branches: [ + StatefulShellBranch( + rootLocation: '/a', navigatorKey: shellNavigatorKey, restorationScopeId: 'scope1', - routes: [ - GoRoute( - path: '/a', - builder: (BuildContext context, GoRouterState state) { - return _DetailsScreen(); - }, - ), - ], + ), + ], + routes: [ + GoRoute( + path: '/a', + builder: (BuildContext context, GoRouterState state) { + return _DetailsScreen(); + }, ), ], ), diff --git a/packages/go_router/test/configuration_test.dart b/packages/go_router/test/configuration_test.dart index fc4fbf4683b7..36a6be39d00f 100644 --- a/packages/go_router/test/configuration_test.dart +++ b/packages/go_router/test/configuration_test.dart @@ -81,115 +81,26 @@ void main() { }); test( - 'throws when StatefulShellRoute sub-route uses incorrect parentNavigatorKey', + 'throws when a sub-route of StatefulShellRoute has a parentNavigatorKey', () { final GlobalKey root = GlobalKey(debugLabel: 'root'); - final GlobalKey keyA = - GlobalKey(debugLabel: 'A'); - final GlobalKey keyB = - GlobalKey(debugLabel: 'B'); - - expect( - () { - RouteConfiguration( - navigatorKey: root, - routes: [ - StatefulShellRoute(branches: [ - ShellRouteBranch( - navigatorKey: keyA, - routes: [ - GoRoute( - path: '/a', - builder: _mockScreenBuilder, - routes: [ - GoRoute( - path: 'details', - builder: _mockScreenBuilder, - parentNavigatorKey: keyB), - ]), - ], - ), - ShellRouteBranch( - navigatorKey: keyB, - routes: [ - GoRoute( - path: '/b', - builder: _mockScreenBuilder, - parentNavigatorKey: keyB), - ], - ), - ], builder: _mockShellBuilder), - ], - redirectLimit: 10, - topRedirect: (BuildContext context, GoRouterState state) { - return null; - }, - ); - }, - throwsAssertionError, - ); - }); - - test('throws when StatefulShellRoute has duplicate navigator keys', () { - final GlobalKey root = - GlobalKey(debugLabel: 'root'); - final GlobalKey keyA = - GlobalKey(debugLabel: 'A'); - final List shellRouteChildren = [ - GoRoute( - path: '/a', builder: _mockScreenBuilder, parentNavigatorKey: keyA), - GoRoute( - path: '/b', builder: _mockScreenBuilder, parentNavigatorKey: keyA), - ]; - expect( - () { - RouteConfiguration( - navigatorKey: root, - routes: [ - StatefulShellRoute(branches: [ - ShellRouteBranch(routes: shellRouteChildren) - ], builder: _mockShellBuilder), - ], - redirectLimit: 10, - topRedirect: (BuildContext context, GoRouterState state) { - return null; - }, - ); - }, - throwsAssertionError, - ); - }); - - test( - 'throws when a child of StatefulShellRoute has an incorrect ' - 'parentNavigatorKey', () { - final GlobalKey root = - GlobalKey(debugLabel: 'root'); - final GlobalKey sectionANavigatorKey = + final GlobalKey someNavigatorKey = GlobalKey(); - final GlobalKey sectionBNavigatorKey = - GlobalKey(); - final GoRoute routeA = GoRoute( - path: '/a', - builder: _mockScreenBuilder, - parentNavigatorKey: sectionBNavigatorKey); - final GoRoute routeB = GoRoute( - path: '/b', - builder: _mockScreenBuilder, - parentNavigatorKey: sectionANavigatorKey); expect( () { RouteConfiguration( navigatorKey: root, routes: [ - StatefulShellRoute(branches: [ - ShellRouteBranch( - routes: [routeA], - navigatorKey: sectionANavigatorKey), - ShellRouteBranch( - routes: [routeB], - navigatorKey: sectionBNavigatorKey), + StatefulShellRoute(branches: [ + StatefulShellBranch(rootLocation: '/a'), + StatefulShellBranch(rootLocation: '/b'), + ], routes: [ + GoRoute( + path: '/a', + builder: _mockScreenBuilder, + parentNavigatorKey: someNavigatorKey), + GoRoute(path: '/b', builder: _mockScreenBuilder) ], builder: _mockShellBuilder), ], redirectLimit: 10, @@ -203,191 +114,75 @@ void main() { }); test( - 'throws when a branch of a StatefulShellRoute has an incorrect ' - 'defaultLocation', () { + 'does not throw when a branch of a StatefulShellRoute has correctly ' + 'configured defaultLocations', () { final GlobalKey root = GlobalKey(debugLabel: 'root'); - final GlobalKey sectionANavigatorKey = - GlobalKey(); - final GlobalKey sectionBNavigatorKey = - GlobalKey(); - expect( - () { - RouteConfiguration( - navigatorKey: root, - routes: [ - StatefulShellRoute(branches: [ - ShellRouteBranch( - defaultLocation: '/x', - navigatorKey: sectionANavigatorKey, + + RouteConfiguration( + navigatorKey: root, + routes: [ + StatefulShellRoute(branches: [ + StatefulShellBranch(rootLocation: '/a'), + StatefulShellBranch(rootLocation: '/b/detail'), + StatefulShellBranch(rootLocation: '/c/detail'), + StatefulShellBranch(rootLocation: '/e'), + ], routes: [ + GoRoute( + path: '/a', + builder: _mockScreenBuilder, + routes: [ + GoRoute( + path: 'detail', + builder: _mockScreenBuilder, + ), + ]), + GoRoute( + path: '/b', + builder: _mockScreenBuilder, + routes: [ + GoRoute( + path: 'detail', + builder: _mockScreenBuilder, + ), + ]), + StatefulShellRoute(branches: [ + StatefulShellBranch(rootLocation: '/c/detail'), + StatefulShellBranch(rootLocation: '/d/detail'), + ], routes: [ + GoRoute( + path: '/c', + builder: _mockScreenBuilder, routes: [ GoRoute( - path: '/a', + path: 'detail', builder: _mockScreenBuilder, ), - ], - ), - ShellRouteBranch( - navigatorKey: sectionBNavigatorKey, + ]), + GoRoute( + path: '/d', + builder: _mockScreenBuilder, routes: [ GoRoute( - path: '/b', + path: 'detail', builder: _mockScreenBuilder, ), - ], - ), - ], builder: _mockShellBuilder), - ], - redirectLimit: 10, - topRedirect: (BuildContext context, GoRouterState state) { - return null; - }, - ); - }, - throwsAssertionError, - ); - }); - - test( - 'throws when a branch of a StatefulShellRoute has a defaultLocation ' - 'that is not a descendant of the same branch', () { - final GlobalKey root = - GlobalKey(debugLabel: 'root'); - final GlobalKey sectionANavigatorKey = - GlobalKey(); - final GlobalKey sectionBNavigatorKey = - GlobalKey(); - expect( - () { - RouteConfiguration( - navigatorKey: root, - routes: [ - StatefulShellRoute(branches: [ - ShellRouteBranch( - defaultLocation: '/b', - navigatorKey: sectionANavigatorKey, + ]), + ], builder: _mockShellBuilder), + ShellRoute( + builder: _mockShellBuilder, + routes: [ + ShellRoute( + builder: _mockShellBuilder, routes: [ GoRoute( - path: '/a', + path: '/e', builder: _mockScreenBuilder, ), ], - ), - ShellRouteBranch( - defaultLocation: '/b', - navigatorKey: sectionBNavigatorKey, - routes: [ - StatefulShellRoute(branches: [ - ShellRouteBranch( - routes: [ - GoRoute( - path: '/b', - builder: _mockScreenBuilder, - ), - ], - ), - ], builder: _mockShellBuilder), - ], - ), - ], builder: _mockShellBuilder), - ], - redirectLimit: 10, - topRedirect: (BuildContext context, GoRouterState state) { - return null; - }, - ); - }, - throwsAssertionError, - ); - }); - - test( - 'does not throw when a branch of a StatefulShellRoute has correctly ' - 'configured defaultLocations', () { - final GlobalKey root = - GlobalKey(debugLabel: 'root'); - - RouteConfiguration( - navigatorKey: root, - routes: [ - StatefulShellRoute(branches: [ - ShellRouteBranch( - routes: [ - GoRoute( - path: '/a', - builder: _mockScreenBuilder, - routes: [ - GoRoute( - path: 'detail', - builder: _mockScreenBuilder, - ), - ]), - ], - ), - ShellRouteBranch( - defaultLocation: '/b/detail', - routes: [ - GoRoute( - path: '/b', - builder: _mockScreenBuilder, - routes: [ - GoRoute( - path: 'detail', - builder: _mockScreenBuilder, - ), - ]), - ], - ), - ShellRouteBranch( - defaultLocation: '/c/detail', - routes: [ - StatefulShellRoute(branches: [ - ShellRouteBranch( - routes: [ - GoRoute( - path: '/c', - builder: _mockScreenBuilder, - routes: [ - GoRoute( - path: 'detail', - builder: _mockScreenBuilder, - ), - ]), - ], - ), - ShellRouteBranch( - defaultLocation: '/d/detail', - routes: [ - GoRoute( - path: '/d', - builder: _mockScreenBuilder, - routes: [ - GoRoute( - path: 'detail', - builder: _mockScreenBuilder, - ), - ]), - ], - ), - ], builder: _mockShellBuilder), + ) ], ), - ShellRouteBranch(routes: [ - ShellRoute( - builder: _mockShellBuilder, - routes: [ - ShellRoute( - builder: _mockShellBuilder, - routes: [ - GoRoute( - path: '/e', - builder: _mockScreenBuilder, - ), - ], - ) - ], - ), - ]), ], builder: _mockShellBuilder), ], redirectLimit: 10, @@ -397,86 +192,6 @@ void main() { ); }); - test( - 'derives the correct defaultLocation for a ShellRouteBranch', - () { - final ShellRouteBranch branchA; - final ShellRouteBranch branchY; - final ShellRouteBranch branchB; - - final RouteConfiguration config = RouteConfiguration( - navigatorKey: GlobalKey(debugLabel: 'root'), - routes: [ - StatefulShellRoute( - builder: (_, __, Widget child) => child, - branches: [ - branchA = ShellRouteBranch(routes: [ - GoRoute( - path: '/a', - builder: _mockScreenBuilder, - routes: [ - GoRoute( - path: 'x', - builder: _mockScreenBuilder, - routes: [ - StatefulShellRoute( - builder: (_, __, Widget child) => child, - branches: [ - branchY = ShellRouteBranch(routes: [ - ShellRoute( - builder: _mockShellBuilder, - routes: [ - GoRoute( - path: 'y1', - builder: _mockScreenBuilder, - ), - GoRoute( - path: 'y2', - builder: _mockScreenBuilder, - ), - ]) - ]) - ]), - ], - ), - ], - ), - ]), - branchB = ShellRouteBranch(routes: [ - ShellRoute( - builder: _mockShellBuilder, - routes: [ - ShellRoute( - builder: _mockShellBuilder, - routes: [ - GoRoute( - path: '/b1', - builder: _mockScreenBuilder, - ), - GoRoute( - path: '/b2', - builder: _mockScreenBuilder, - ), - ], - ) - ], - ), - ]), - ], - ), - ], - redirectLimit: 10, - topRedirect: (BuildContext context, GoRouterState state) { - return null; - }, - ); - - expect('/a', config.findShellRouteBranchDefaultLocation(branchA)); - expect('/a/x/y1', config.findShellRouteBranchDefaultLocation(branchY)); - expect('/b1', config.findShellRouteBranchDefaultLocation(branchB)); - }, - ); - test( 'throws when there is a GoRoute ancestor with a different parentNavigatorKey', () { diff --git a/packages/go_router/test/delegate_test.dart b/packages/go_router/test/delegate_test.dart index af0a693e7236..49b9274c6713 100644 --- a/packages/go_router/test/delegate_test.dart +++ b/packages/go_router/test/delegate_test.dart @@ -37,8 +37,13 @@ Future createGoRouterWithStatefulShellRoute( routes: [ GoRoute(path: '/', builder: (_, __) => const DummyStatefulWidget()), GoRoute(path: '/a', builder: (_, __) => const DummyStatefulWidget()), - StatefulShellRoute(branches: [ - ShellRouteBranch(routes: [ + StatefulShellRoute( + branches: [ + StatefulShellBranch(rootLocation: '/c'), + StatefulShellBranch(rootLocation: '/d'), + ], + builder: (_, __, Widget child) => child, + routes: [ GoRoute( path: '/c', builder: (_, __) => const DummyStatefulWidget(), @@ -50,8 +55,6 @@ Future createGoRouterWithStatefulShellRoute( path: 'c2', builder: (_, __) => const DummyStatefulWidget()), ]), - ]), - ShellRouteBranch(routes: [ GoRoute( path: '/d', builder: (_, __) => const DummyStatefulWidget(), @@ -60,8 +63,8 @@ Future createGoRouterWithStatefulShellRoute( path: 'd1', builder: (_, __) => const DummyStatefulWidget()), ]), - ]), - ], builder: (_, __, Widget child) => child), + ], + ), ], ); await tester.pumpWidget(MaterialApp.router( diff --git a/packages/go_router/test/go_router_test.dart b/packages/go_router/test/go_router_test.dart index 0880d7acae6d..ef00fa85676c 100644 --- a/packages/go_router/test/go_router_test.dart +++ b/packages/go_router/test/go_router_test.dart @@ -2116,32 +2116,38 @@ void main() { routeState = StatefulShellRoute.of(context); return child; }, - branches: [ - ShellRouteBranch(routes: [ - GoRoute( - path: '/a', - builder: (BuildContext context, GoRouterState state) => - const Text('Screen A'), - ), - ]), - ShellRouteBranch(routes: [ - GoRoute( - path: '/family/:fid', + routes: [ + GoRoute( + path: '/a', + builder: (BuildContext context, GoRouterState state) => + const Text('Screen A'), + ), + GoRoute( + path: '/family', builder: (BuildContext context, GoRouterState state) => - FamilyScreen(state.params['fid']!), - routes: [ + const Text('Families'), + routes: [ GoRoute( - path: 'person/:pid', - builder: (BuildContext context, GoRouterState state) { - final String fid = state.params['fid']!; - final String pid = state.params['pid']!; + path: ':fid', + builder: (BuildContext context, GoRouterState state) => + FamilyScreen(state.params['fid']!), + routes: [ + GoRoute( + path: 'person/:pid', + builder: (BuildContext context, GoRouterState state) { + final String fid = state.params['fid']!; + final String pid = state.params['pid']!; - return PersonScreen(fid, pid); - }, - ), - ], - ), - ]), + return PersonScreen(fid, pid); + }, + ), + ], + ) + ]), + ], + branches: [ + StatefulShellBranch(rootLocation: '/a'), + StatefulShellBranch(rootLocation: '/family'), ], ), ]; @@ -2155,25 +2161,28 @@ void main() { router.go(loc); await tester.pumpAndSettle(); List matches = router.routerDelegate.matches.matches; + RouteMatch last = + router.routerDelegate.matches.lastWithEffectiveEncodedParams; expect(router.location, loc); - expect(matches, hasLength(3)); + expect(matches, hasLength(4)); expect(find.byType(PersonScreen), findsOneWidget); - expect(matches.last.decodedParams['fid'], fid); - expect(matches.last.decodedParams['pid'], pid); + expect(last.decodedParams['fid'], fid); + expect(last.decodedParams['pid'], pid); - routeState?.goBranch(0); + routeState?.goBranch(index: 0); await tester.pumpAndSettle(); expect(find.text('Screen A'), findsOneWidget); expect(find.byType(PersonScreen), findsNothing); - routeState?.goBranch(1); + routeState?.goBranch(index: 1); await tester.pumpAndSettle(); expect(find.text('Screen A'), findsNothing); expect(find.byType(PersonScreen), findsOneWidget); matches = router.routerDelegate.matches.matches; - expect(matches.last.decodedParams['fid'], fid); - expect(matches.last.decodedParams['pid'], pid); + last = router.routerDelegate.matches.lastWithEffectiveEncodedParams; + expect(last.decodedParams['fid'], fid); + expect(last.decodedParams['pid'], pid); }); testWidgets('goNames should allow dynamics values for queryParams', @@ -2618,31 +2627,31 @@ void main() { final List routes = [ StatefulShellRoute( builder: (_, __, Widget child) => child, - branches: [ - ShellRouteBranch(routes: [ - GoRoute( - path: '/a', - builder: (BuildContext context, GoRouterState state) => - const Text('Screen A'), - routes: [ - GoRoute( - path: 'detailA', - builder: (BuildContext context, GoRouterState state) => - Column(children: [ - const Text('Screen A Detail'), - DummyStatefulWidget(key: statefulWidgetKey), - ]), - ), - ], - ), - ]), - ShellRouteBranch(routes: [ - GoRoute( - path: '/b', - builder: (BuildContext context, GoRouterState state) => - const Text('Screen B'), - ), - ]), + routes: [ + GoRoute( + path: '/a', + builder: (BuildContext context, GoRouterState state) => + const Text('Screen A'), + routes: [ + GoRoute( + path: 'detailA', + builder: (BuildContext context, GoRouterState state) => + Column(children: [ + const Text('Screen A Detail'), + DummyStatefulWidget(key: statefulWidgetKey), + ]), + ), + ], + ), + GoRoute( + path: '/b', + builder: (BuildContext context, GoRouterState state) => + const Text('Screen B'), + ), + ], + branches: [ + StatefulShellBranch(rootLocation: '/a'), + StatefulShellBranch(rootLocation: '/b'), ], ), ]; @@ -2686,39 +2695,37 @@ void main() { final List routes = [ StatefulShellRoute( builder: (_, __, Widget child) => child, - branches: [ - ShellRouteBranch( - navigatorKey: sectionANavigatorKey, - routes: [ - GoRoute( - path: '/a', - builder: (BuildContext context, GoRouterState state) => - const Text('Screen A'), - routes: [ - GoRoute( - path: 'detailA', - builder: (BuildContext context, GoRouterState state) => - const Text('Screen A Detail'), - ), - ], - ), - ]), - ShellRouteBranch( - navigatorKey: sectionBNavigatorKey, - routes: [ - GoRoute( - path: '/b', - builder: (BuildContext context, GoRouterState state) => - const Text('Screen B'), - routes: [ - GoRoute( - path: 'detailB', - builder: (BuildContext context, GoRouterState state) => - const Text('Screen B Detail'), - ), - ], - ), - ]), + routes: [ + GoRoute( + path: '/a', + builder: (BuildContext context, GoRouterState state) => + const Text('Screen A'), + routes: [ + GoRoute( + path: 'detailA', + builder: (BuildContext context, GoRouterState state) => + const Text('Screen A Detail'), + ), + ], + ), + GoRoute( + path: '/b', + builder: (BuildContext context, GoRouterState state) => + const Text('Screen B'), + routes: [ + GoRoute( + path: 'detailB', + builder: (BuildContext context, GoRouterState state) => + const Text('Screen B Detail'), + ), + ], + ), + ], + branches: [ + StatefulShellBranch( + rootLocation: '/a', navigatorKey: sectionANavigatorKey), + StatefulShellBranch( + rootLocation: '/b', navigatorKey: sectionBNavigatorKey), ], ), ]; @@ -2760,21 +2767,21 @@ void main() { routeState = StatefulShellRoute.of(context); return child; }, - branches: [ - ShellRouteBranch(routes: [ - GoRoute( - path: '/a', - builder: (BuildContext context, GoRouterState state) => - const Text('Screen A'), - ), - ]), - ShellRouteBranch(routes: [ - GoRoute( - path: '/b', - builder: (BuildContext context, GoRouterState state) => - Text('Screen B - ${state.extra}'), - ), - ]), + routes: [ + GoRoute( + path: '/a', + builder: (BuildContext context, GoRouterState state) => + const Text('Screen A'), + ), + GoRoute( + path: '/b', + builder: (BuildContext context, GoRouterState state) => + Text('Screen B - ${state.extra}'), + ), + ], + branches: [ + StatefulShellBranch(rootLocation: '/a'), + StatefulShellBranch(rootLocation: '/b'), ], ), ]; @@ -2788,12 +2795,12 @@ void main() { expect(find.text('Screen A'), findsNothing); expect(find.text('Screen B - X'), findsOneWidget); - routeState!.goBranch(0); + routeState!.goBranch(index: 0); await tester.pumpAndSettle(); expect(find.text('Screen A'), findsOneWidget); expect(find.text('Screen B - X'), findsNothing); - routeState!.goBranch(1); + routeState!.goBranch(index: 1); await tester.pumpAndSettle(); expect(find.text('Screen A'), findsNothing); expect(find.text('Screen B - X'), findsOneWidget); @@ -2818,21 +2825,21 @@ void main() { routeState = StatefulShellRoute.of(context); return child; }, - branches: [ - ShellRouteBranch(routes: [ - GoRoute( - path: '/a', - builder: (BuildContext context, GoRouterState state) => - const Text('Screen A'), - ), - ]), - ShellRouteBranch(routes: [ - GoRoute( - path: '/b', - builder: (BuildContext context, GoRouterState state) => - const Text('Screen B'), - ), - ]), + routes: [ + GoRoute( + path: '/a', + builder: (BuildContext context, GoRouterState state) => + const Text('Screen A'), + ), + GoRoute( + path: '/b', + builder: (BuildContext context, GoRouterState state) => + Text('Screen B - ${state.extra ?? ''}'), + ), + ], + branches: [ + StatefulShellBranch(rootLocation: '/a'), + StatefulShellBranch(rootLocation: '/b'), ], ), ]; @@ -2844,7 +2851,7 @@ void main() { router.go('/b'); await tester.pumpAndSettle(); expect(find.text('Screen A'), findsNothing); - expect(find.text('Screen B'), findsOneWidget); + expect(find.text('Screen B - '), findsOneWidget); router.push('/common', extra: 'X'); await tester.pumpAndSettle(); @@ -2852,11 +2859,11 @@ void main() { expect(find.text('Screen B'), findsNothing); expect(find.text('Common - X'), findsOneWidget); - routeState!.goBranch(0); + routeState!.goBranch(index: 0); await tester.pumpAndSettle(); expect(find.text('Screen A'), findsOneWidget); - routeState!.goBranch(1); + routeState!.goBranch(index: 1); await tester.pumpAndSettle(); expect(find.text('Screen A'), findsNothing); expect(find.text('Screen B'), findsNothing); @@ -2882,55 +2889,53 @@ void main() { StatefulShellRoute( builder: (BuildContext context, GoRouterState state, Widget child) => child, - branches: [ - ShellRouteBranch(routes: [ - GoRoute( - path: '/a', - builder: (BuildContext context, GoRouterState state) => - DummyStatefulWidget(key: statefulWidgetKeyA), - ), - ]), - ShellRouteBranch(routes: [ - GoRoute( - path: '/b', - builder: (BuildContext context, GoRouterState state) => - DummyStatefulWidget(key: statefulWidgetKeyB), - ), - ]), + routes: [ + GoRoute( + path: '/a', + builder: (BuildContext context, GoRouterState state) => + DummyStatefulWidget(key: statefulWidgetKeyA), + ), + GoRoute( + path: '/b', + builder: (BuildContext context, GoRouterState state) => + DummyStatefulWidget(key: statefulWidgetKeyB), + ), + ], + branches: [ + StatefulShellBranch(rootLocation: '/a'), + StatefulShellBranch(rootLocation: '/b'), ], ), StatefulShellRoute( - preloadBranches: true, builder: (BuildContext context, GoRouterState state, Widget child) => - child, - branches: [ - ShellRouteBranch(routes: [ - GoRoute( - path: '/c', - builder: (BuildContext context, GoRouterState state) => - DummyStatefulWidget(key: statefulWidgetKeyC), - ), - ]), - ShellRouteBranch(routes: [ - GoRoute( - path: '/d', + Scaffold(body: child), + routes: [ + GoRoute( + path: '/c', + builder: (BuildContext context, GoRouterState state) => + DummyStatefulWidget(key: statefulWidgetKeyC), + ), + GoRoute( + path: '/d', + builder: (BuildContext context, GoRouterState state) => + DummyStatefulWidget(key: statefulWidgetKeyD), + ), + GoRoute( + path: '/e', builder: (BuildContext context, GoRouterState state) => - DummyStatefulWidget(key: statefulWidgetKeyD), - ), - ]), - ShellRouteBranch(routes: [ - GoRoute( - path: '/e', - builder: (BuildContext context, GoRouterState state) => - const Text('E'), - routes: [ - GoRoute( - path: 'details', - builder: (BuildContext context, GoRouterState state) => - DummyStatefulWidget(key: statefulWidgetKeyE), - ), - ]), - ]), + const Text('E'), + routes: [ + GoRoute( + path: 'details', + builder: (BuildContext context, GoRouterState state) => + DummyStatefulWidget(key: statefulWidgetKeyE), + ), + ]), + ], + branches: [ + StatefulShellBranch(rootLocation: '/c', preload: true), + StatefulShellBranch(rootLocation: '/d', preload: true), + StatefulShellBranch(rootLocation: '/e', preload: true), ], ), ]; @@ -2972,49 +2977,48 @@ void main() { routeState = StatefulShellRoute.of(context); return child; }, - branches: [ - ShellRouteBranch(routes: [ - GoRoute( - path: '/a', - builder: (BuildContext context, GoRouterState state) => - const Text('Screen A'), - ), - ]), - ShellRouteBranch(routes: [ - GoRoute( - path: '/b', - builder: (BuildContext context, GoRouterState state) => - const Text('Screen B'), - routes: [ - GoRoute( - path: 'details1', - builder: (BuildContext context, GoRouterState state) => - const Text('Screen B Detail1'), - ), - GoRoute( - path: 'details2', - builder: (BuildContext context, GoRouterState state) => - const Text('Screen B Detail2'), - ), - ], - ), - ]), - ShellRouteBranch(routes: [ - GoRoute( - path: '/c', - redirect: (_, __) => '/c/main2', - ), - GoRoute( - path: '/c/main1', - builder: (BuildContext context, GoRouterState state) => - const Text('Screen C1'), - ), - GoRoute( - path: '/c/main2', - builder: (BuildContext context, GoRouterState state) => - const Text('Screen C2'), - ), - ]), + routes: [ + GoRoute( + path: '/a', + builder: (BuildContext context, GoRouterState state) => + const Text('Screen A'), + ), + GoRoute( + path: '/b', + builder: (BuildContext context, GoRouterState state) => + const Text('Screen B'), + routes: [ + GoRoute( + path: 'details1', + builder: (BuildContext context, GoRouterState state) => + const Text('Screen B Detail1'), + ), + GoRoute( + path: 'details2', + builder: (BuildContext context, GoRouterState state) => + const Text('Screen B Detail2'), + ), + ], + ), + GoRoute( + path: '/c', + redirect: (_, __) => '/c/main2', + ), + GoRoute( + path: '/c/main1', + builder: (BuildContext context, GoRouterState state) => + const Text('Screen C1'), + ), + GoRoute( + path: '/c/main2', + builder: (BuildContext context, GoRouterState state) => + const Text('Screen C2'), + ), + ], + branches: [ + StatefulShellBranch(rootLocation: '/a'), + StatefulShellBranch(rootLocation: '/b'), + StatefulShellBranch(rootLocation: '/c'), ], ), ]; @@ -3035,19 +3039,19 @@ void main() { expect(find.text('Screen A'), findsOneWidget); expect(find.text('Screen B Detail'), findsNothing); - routeState!.goBranch(1); + routeState!.goBranch(index: 1); await tester.pumpAndSettle(); expect(find.text('Screen A'), findsNothing); expect(find.text('Screen B Detail1'), findsOneWidget); - routeState!.goBranch(2); + routeState!.goBranch(index: 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); + routeState!.goBranch(index: 1); await tester.pumpAndSettle(); expect(find.text('Screen A'), findsNothing); expect(find.text('Screen B Detail2'), findsOneWidget); @@ -3066,35 +3070,35 @@ void main() { routeState = StatefulShellRoute.of(context); return child; }, - branches: [ - ShellRouteBranch(routes: [ - GoRoute( - path: '/a', - builder: (BuildContext context, GoRouterState state) => - const Text('Screen A'), - routes: [ - GoRoute( - path: 'detail', - builder: (BuildContext context, GoRouterState state) => - const Text('Screen A Detail'), - ), - ], - ), - ]), - ShellRouteBranch(routes: [ - GoRoute( - path: '/b', - builder: (BuildContext context, GoRouterState state) => - const Text('Screen B'), - routes: [ - GoRoute( - path: 'detail', - builder: (BuildContext context, GoRouterState state) => - const Text('Screen B Detail'), - ), - ], - ), - ]), + routes: [ + GoRoute( + path: '/a', + builder: (BuildContext context, GoRouterState state) => + const Text('Screen A'), + routes: [ + GoRoute( + path: 'detail', + builder: (BuildContext context, GoRouterState state) => + const Text('Screen A Detail'), + ), + ], + ), + GoRoute( + path: '/b', + builder: (BuildContext context, GoRouterState state) => + const Text('Screen B'), + routes: [ + GoRoute( + path: 'detail', + builder: (BuildContext context, GoRouterState state) => + const Text('Screen B Detail'), + ), + ], + ), + ], + branches: [ + StatefulShellBranch(rootLocation: '/a'), + StatefulShellBranch(rootLocation: '/b'), ], ), ]; @@ -3114,7 +3118,7 @@ void main() { expect(find.text('Screen B'), findsOneWidget); expect(find.text('Screen B Detail'), findsNothing); - routeState!.goBranch(0); + routeState!.goBranch(index: 0); await tester.pumpAndSettle(); expect(find.text('Screen A'), findsOneWidget); expect(find.text('Screen A Detail'), findsNothing); @@ -3264,37 +3268,37 @@ void main() { body: child, ); }, - branches: [ - ShellRouteBranch(routes: [ - GoRoute( - path: '/a', - builder: (BuildContext context, _) { - return const Scaffold( - body: Text('Screen A'), - ); - }, - ), - ]), - ShellRouteBranch(routes: [ - GoRoute( - path: '/b', - builder: (BuildContext context, _) { - return const Scaffold( - body: Text('Screen B'), - ); - }, - routes: [ - GoRoute( - path: 'detail', - builder: (BuildContext context, _) { - return const Scaffold( - body: Text('Screen B detail'), - ); - }, - ), - ], - ), - ]), + routes: [ + GoRoute( + path: '/a', + builder: (BuildContext context, _) { + return const Scaffold( + body: Text('Screen A'), + ); + }, + ), + GoRoute( + path: '/b', + builder: (BuildContext context, _) { + return const Scaffold( + body: Text('Screen B'), + ); + }, + routes: [ + GoRoute( + path: 'detail', + builder: (BuildContext context, _) { + return const Scaffold( + body: Text('Screen B detail'), + ); + }, + ), + ], + ), + ], + branches: [ + StatefulShellBranch(rootLocation: '/a'), + StatefulShellBranch(rootLocation: '/b'), ], ), ], From ee2a845d196d11d1c6d36626278b21c0cbbe9adf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20Lo=CC=88fstrand?= Date: Wed, 30 Nov 2022 14:27:24 +0100 Subject: [PATCH 056/112] Introduced an internal branch Navigator proxy widget to make the API simpler (always returning a concrete Widget for each branch). Added temporary workaround to get correct pop behaviour. --- .../lib/stateful_nested_navigation.dart | 11 +- packages/go_router/lib/src/builder.dart | 29 ++-- packages/go_router/lib/src/delegate.dart | 17 +- packages/go_router/lib/src/matching.dart | 19 ++- .../src/misc/stateful_navigation_shell.dart | 146 ++++++++++++++---- packages/go_router/lib/src/router.dart | 1 + packages/go_router/lib/src/state.dart | 25 +-- packages/go_router/test/builder_test.dart | 12 +- 8 files changed, 184 insertions(+), 76 deletions(-) diff --git a/packages/go_router/example/lib/stateful_nested_navigation.dart b/packages/go_router/example/lib/stateful_nested_navigation.dart index feee84eb52a0..fed800bfa35d 100644 --- a/packages/go_router/example/lib/stateful_nested_navigation.dart +++ b/packages/go_router/example/lib/stateful_nested_navigation.dart @@ -349,16 +349,7 @@ class TabbedRootScreen extends StatelessWidget { /// Constructs a TabbedRootScreen const TabbedRootScreen({Key? key}) : super(key: key); - Widget _child(StatefulShellBranchState branchState) { - // TabBarView will cache it's root widget, so we need to reevaluate - // the child (using a Builder) in case it's null. - return branchState.child != null - ? branchState.child! - : Builder( - builder: (BuildContext context) => - StatefulShellBranch.of(context).child ?? - const SizedBox.expand()); - } + Widget _child(StatefulShellBranchState branchState) => branchState.child; @override Widget build(BuildContext context) { diff --git a/packages/go_router/lib/src/builder.dart b/packages/go_router/lib/src/builder.dart index 4646c7962ca1..059258cb9eec 100644 --- a/packages/go_router/lib/src/builder.dart +++ b/packages/go_router/lib/src/builder.dart @@ -102,7 +102,7 @@ class RouteBuilder { _buildNavigator( pop, buildPages( - context, matchList, pop, routerNeglect, navigatorKey, registry), + context, matchList, 0, pop, routerNeglect, navigatorKey, registry), navigatorKey, observers: observers, ), @@ -115,6 +115,7 @@ class RouteBuilder { List> buildPages( BuildContext context, RouteMatchList matchList, + int startIndex, VoidCallback onPop, bool routerNeglect, GlobalKey navigatorKey, @@ -122,8 +123,8 @@ class RouteBuilder { try { final Map, List>> keyToPage = , List>>{}; - _buildRecursive(context, matchList, 0, onPop, routerNeglect, keyToPage, - navigatorKey, registry); + _buildRecursive(context, matchList, startIndex, onPop, routerNeglect, + keyToPage, navigatorKey, registry); return keyToPage[navigatorKey]!; } on _RouteBuilderError catch (e) { return >[ @@ -216,11 +217,8 @@ class RouteBuilder { child = _buildNavigator( pop, keyToPages[shellNavigatorKey]!, shellNavigatorKey, restorationScopeId: branch.restorationScopeId); - final StatefulShellBranchState shellNavigatorState = - StatefulShellBranchState( - branch: branch, child: child, matchList: matchList); - child = _buildStatefulNavigationShell( - route, state, branches, shellNavigatorState, pop, registry); + child = _buildStatefulNavigationShell(route, state, branches, branch, + child as Navigator, matchList, pop, registry); } else if (route is ShellRoute) { // The key to provide to the shell route's Navigator. shellNavigatorKey = route.navigatorKey; @@ -270,7 +268,9 @@ class RouteBuilder { StatefulShellRoute shellRoute, GoRouterState shellRouterState, List branches, - StatefulShellBranchState currentBranchState, + StatefulShellBranch currentBranch, + Navigator currentNavigator, + RouteMatchList currentMatchList, VoidCallback pop, Map, GoRouterState> registry, ) { @@ -279,13 +279,16 @@ class RouteBuilder { shellRoute: shellRoute, shellGoRouterState: shellRouterState, branches: branches, - currentBranchState: currentBranchState, + currentBranch: currentBranch, + currentNavigator: currentNavigator, + currentMatchList: currentMatchList, branchNavigatorBuilder: (BuildContext context, - RouteMatchList navigatorMatchList, + RouteMatchList matchList, + int startIndex, GlobalKey navigatorKey, String? restorationScopeId) { - final List> pages = buildPages( - context, navigatorMatchList, pop, true, navigatorKey, registry); + final List> pages = buildPages(context, matchList, + startIndex, pop, true, navigatorKey, registry); return _buildNavigator(pop, pages, navigatorKey, restorationScopeId: restorationScopeId); }); diff --git a/packages/go_router/lib/src/delegate.dart b/packages/go_router/lib/src/delegate.dart index 4480d121dc0d..22b8866efdf5 100644 --- a/packages/go_router/lib/src/delegate.dart +++ b/packages/go_router/lib/src/delegate.dart @@ -9,6 +9,7 @@ import 'package:flutter/widgets.dart'; import 'builder.dart'; import 'configuration.dart'; +import 'information_provider.dart'; import 'match.dart'; import 'matching.dart'; import 'typedefs.dart'; @@ -19,6 +20,7 @@ class GoRouterDelegate extends RouterDelegate /// Constructor for GoRouter's implementation of the RouterDelegate base /// class. GoRouterDelegate({ + required GoRouteInformationProvider routeInformationProvider, required RouteConfiguration configuration, required GoRouterBuilderWithNav builderWithNav, required GoRouterPageBuilder? errorPageBuilder, @@ -26,7 +28,8 @@ class GoRouterDelegate extends RouterDelegate required List observers, required this.routerNeglect, String? restorationScopeId, - }) : _configuration = configuration, + }) : _routeInformationProvider = routeInformationProvider, + _configuration = configuration, builder = RouteBuilder( configuration: configuration, builderWithNav: builderWithNav, @@ -45,6 +48,8 @@ class GoRouterDelegate extends RouterDelegate RouteMatchList _matchList = RouteMatchList.empty; + late final GoRouteInformationProvider _routeInformationProvider; + /// Stores the number of times each route route has been pushed. /// /// This is used to generate a unique key for each route. @@ -150,12 +155,20 @@ class GoRouterDelegate extends RouterDelegate /// Pop the top page off the GoRouter's page stack. void pop() { + final bool didPush = _matchList.last is ImperativeRouteMatch; _matchList.pop(); assert(() { _debugAssertMatchListNotEmpty(); return true; }()); - notifyListeners(); + if (didPush) { + notifyListeners(); + } else { + // TODO(tolo): Temporary workaround (here and in RouteMatchList) to get expected pop behaviour + _routeInformationProvider.value = RouteInformation( + location: _matchList.lastMatchUri.toString(), + state: _matchList.last.extra); + } } /// Replaces the top-most page of the page stack with the given one. diff --git a/packages/go_router/lib/src/matching.dart b/packages/go_router/lib/src/matching.dart index e076a89473a5..345dc3f53737 100644 --- a/packages/go_router/lib/src/matching.dart +++ b/packages/go_router/lib/src/matching.dart @@ -72,6 +72,17 @@ class RouteMatchList { return buffer.toString(); } + static String _addQueryParams( + String loc, Map queryParametersAll) { + final Uri uri = Uri.parse(loc); + assert(uri.queryParameters.isEmpty); + return Uri( + path: uri.path, + queryParameters: + queryParametersAll.isEmpty ? null : queryParametersAll) + .toString(); + } + final List _matches; /// the full path pattern that matches the uri. @@ -81,9 +92,15 @@ class RouteMatchList { /// Parameters for the matched route, URI-encoded. final Map pathParameters; - /// The uri of the current match. + /// The uri of the original match. final Uri uri; + /// The uri of the last match. + Uri get lastMatchUri => _matches.isEmpty + ? Uri(queryParameters: uri.queryParametersAll) + : Uri.parse( + _addQueryParams(_matches.last.subloc, uri.queryParametersAll)); + /// Returns true if there are no matches. bool get isEmpty => _matches.isEmpty; diff --git a/packages/go_router/lib/src/misc/stateful_navigation_shell.dart b/packages/go_router/lib/src/misc/stateful_navigation_shell.dart index b12f08acfeb4..567c901dfb93 100644 --- a/packages/go_router/lib/src/misc/stateful_navigation_shell.dart +++ b/packages/go_router/lib/src/misc/stateful_navigation_shell.dart @@ -32,10 +32,11 @@ class InheritedStatefulNavigationShell extends InheritedWidget { } } -/// Builder function for a route branch navigator -typedef ShellRouteBranchNavigatorBuilder = Navigator Function( +/// Builder function for preloading a route branch navigator +typedef ShellRouteBranchNavigatorPreloadBuilder = Navigator Function( BuildContext context, RouteMatchList navigatorMatchList, + int startIndex, GlobalKey navigatorKey, String? restorationScopeId, ); @@ -62,7 +63,9 @@ class StatefulNavigationShell extends StatefulWidget { required this.shellRoute, required this.shellGoRouterState, required this.branches, - required this.currentBranchState, + required this.currentBranch, + required this.currentNavigator, + required this.currentMatchList, required this.branchNavigatorBuilder, super.key, }); @@ -76,14 +79,20 @@ class StatefulNavigationShell extends StatefulWidget { /// The [GoRouterState] for the navigation shell. final GoRouterState shellGoRouterState; - /// The currently active set of [StatefulShellBranchState]s. + /// The currently active set of [StatefulShellBranch]s. final List branches; - /// The [StatefulShellBranchState] for the current location. - final StatefulShellBranchState currentBranchState; + /// The [StatefulShellBranch] for the current location + final StatefulShellBranch currentBranch; + + /// The navigator for the currently active route branch + final Navigator currentNavigator; + + /// The RouteMatchList for the current location + final RouteMatchList currentMatchList; /// Builder for route branch navigators (used for preloading). - final ShellRouteBranchNavigatorBuilder branchNavigatorBuilder; + final ShellRouteBranchNavigatorPreloadBuilder branchNavigatorBuilder; @override State createState() => StatefulNavigationShellState(); @@ -91,11 +100,17 @@ class StatefulNavigationShell extends StatefulWidget { /// State for StatefulNavigationShell. class StatefulNavigationShellState extends State { + final Map _navigatorCache = {}; + late StatefulShellRouteState _routeState; + Navigator? _navigatorForBranch(StatefulShellBranch branch) { + return _navigatorCache[branch.navigatorKey]; + } + int _findCurrentIndex() { - final int index = widget.branches.indexWhere( - (StatefulShellBranch e) => e == widget.currentBranchState.branch); + final int index = widget.branches.indexWhere((StatefulShellBranch e) => + e.navigatorKey == widget.currentBranch.navigatorKey); assert(index >= 0); return index; } @@ -117,19 +132,19 @@ class StatefulNavigationShellState extends State { } Future _preloadBranch( - StatefulShellBranchState navigatorState) { + StatefulShellBranchState branchState) { // Parse a RouteMatchList from the default location of the route branch and // handle any redirects final GoRouteInformationParser parser = GoRouter.of(context).routeInformationParser; final Future routeMatchList = parser.parseRouteInformationWithDependencies( - RouteInformation(location: navigatorState.branch.defaultLocation), + RouteInformation(location: branchState.branch.defaultLocation), context); StatefulShellBranchState createBranchNavigator(RouteMatchList matchList) { // Find the index of the branch root route in the match list - final StatefulShellBranch branch = navigatorState.branch; + final StatefulShellBranch branch = branchState.branch; final int shellRouteIndex = matchList.matches .indexWhere((RouteMatch e) => e.route == widget.shellRoute); // Keep only the routes from and below the root route in the match list and @@ -137,20 +152,21 @@ class StatefulNavigationShellState extends State { Navigator? navigator; if (shellRouteIndex >= 0 && shellRouteIndex < (matchList.matches.length - 1)) { - final RouteMatchList navigatorMatchList = RouteMatchList( - matchList.matches.sublist(shellRouteIndex + 1), - matchList.uri, - matchList.pathParameters); - navigator = widget.branchNavigatorBuilder(context, navigatorMatchList, - branch.navigatorKey, branch.restorationScopeId); + navigator = widget.branchNavigatorBuilder( + context, + matchList, + shellRouteIndex + 1, + branch.navigatorKey, + branch.restorationScopeId); } - return navigatorState.copy(child: navigator, matchList: matchList); + return _copyStatefulShellBranchState(branchState, + navigator: navigator, matchList: matchList); } return routeMatchList.then(createBranchNavigator); } - void _updateRouteBranchState(StatefulShellBranchState navigatorState, + void _updateRouteBranchState(StatefulShellBranchState branchState, {int? currentIndex}) { final List branches = widget.branches; final List existingStates = @@ -159,12 +175,12 @@ class StatefulNavigationShellState extends State { []; for (final StatefulShellBranch branch in branches) { - if (branch.navigatorKey == navigatorState.navigatorKey) { - newStates.add(navigatorState); + if (branch.navigatorKey == branchState.navigatorKey) { + newStates.add(branchState); } else { newStates.add(existingStates.firstWhereOrNull( (StatefulShellBranchState e) => e.branch == branch) ?? - StatefulShellBranchState(branch: branch)); + _createStatefulShellBranchState(branch)); } } @@ -177,9 +193,8 @@ class StatefulNavigationShellState extends State { void _preloadBranches() { final List states = _routeState.branchStates; for (StatefulShellBranchState state in states) { - if (state.branch.preload && state.child == null) { - // Set a placeholder widget as child to prevent repeated preloading - state = state.copy(child: const SizedBox.shrink()); + if (state.branch.preload && !state.preloading) { + state = _copyStatefulShellBranchState(state, preloading: true); _preloadBranch(state).then((StatefulShellBranchState navigatorState) { setState(() { _updateRouteBranchState(navigatorState); @@ -191,8 +206,12 @@ class StatefulNavigationShellState extends State { void _updateRouteStateFromWidget() { final int index = _findCurrentIndex(); + final StatefulShellBranch branch = widget.currentBranch; + _updateRouteBranchState( - widget.currentBranchState, + _createStatefulShellBranchState(branch, + navigator: widget.currentNavigator, + matchList: widget.currentMatchList), currentIndex: index, ); @@ -202,13 +221,47 @@ class StatefulNavigationShellState extends State { void _resetState() { final StatefulShellBranchState navigatorState = _routeState.currentBranchState; + _navigatorCache.clear(); _setupInitialStatefulShellRouteState(); GoRouter.of(context).go(navigatorState.branch.defaultLocation); } + StatefulShellBranchState _copyStatefulShellBranchState( + StatefulShellBranchState branchState, + {Navigator? navigator, + RouteMatchList? matchList, + bool preloading = false}) { + if (navigator != null) { + _navigatorCache[branchState.navigatorKey] = navigator; + } + final _BranchNavigatorProxy branchWidget = + branchState.child as _BranchNavigatorProxy; + return branchState.copy( + child: branchWidget.copy(preloading: preloading), + matchList: matchList, + ); + } + + StatefulShellBranchState _createStatefulShellBranchState( + StatefulShellBranch branch, + {Navigator? navigator, + RouteMatchList? matchList}) { + if (navigator != null) { + _navigatorCache[branch.navigatorKey] = navigator; + } + return StatefulShellBranchState( + branch: branch, + child: _BranchNavigatorProxy( + branch: branch, + navigatorForBranch: _navigatorForBranch, + ), + matchList: matchList, + ); + } + void _setupInitialStatefulShellRouteState() { final List states = widget.branches - .map((StatefulShellBranch e) => StatefulShellBranchState(branch: e)) + .map((StatefulShellBranch e) => _createStatefulShellBranchState(e)) .toList(); _routeState = StatefulShellRouteState( @@ -256,6 +309,36 @@ class StatefulNavigationShellState extends State { } } +typedef _NavigatorForBranch = Navigator? Function(StatefulShellBranch); + +/// Widget that serves as the proxy for a branch Navigator Widget, which +/// possibly hasn't been created yet. +class _BranchNavigatorProxy extends StatelessWidget { + const _BranchNavigatorProxy( + {required this.branch, + required this.navigatorForBranch, + this.preloading = false, + super.key}); + + _BranchNavigatorProxy copy({bool preloading = false}) { + return _BranchNavigatorProxy( + branch: branch, + preloading: preloading, + navigatorForBranch: navigatorForBranch, + key: key, + ); + } + + final StatefulShellBranch branch; + final _NavigatorForBranch navigatorForBranch; + final bool preloading; + + @override + Widget build(BuildContext context) { + return navigatorForBranch(branch) ?? const SizedBox.shrink(); + } +} + /// Default implementation of a container widget for the [Navigator]s of the /// route branches. This implementation uses an [IndexedStack] as a container. class _IndexedStackedRouteBranchContainer extends StatelessWidget { @@ -279,15 +362,11 @@ class _IndexedStackedRouteBranchContainer extends StatelessWidget { Widget _buildRouteBranchContainer(BuildContext context, bool isActive, StatefulShellBranchState navigatorState) { - final Widget? navigator = navigatorState.child; - if (navigator == null) { - return const SizedBox.shrink(); - } return Offstage( offstage: !isActive, child: TickerMode( enabled: isActive, - child: navigator, + child: navigatorState.child, ), ); } @@ -314,4 +393,5 @@ extension StatefulShellBranchResolver on StatefulShellRoute { extension _StatefulShellBranchStateHelper on StatefulShellBranchState { GlobalKey get navigatorKey => branch.navigatorKey; + bool get preloading => (child as _BranchNavigatorProxy).preloading; } diff --git a/packages/go_router/lib/src/router.dart b/packages/go_router/lib/src/router.dart index 120bfd244fdc..e604bdf11b2b 100644 --- a/packages/go_router/lib/src/router.dart +++ b/packages/go_router/lib/src/router.dart @@ -87,6 +87,7 @@ class GoRouter extends ChangeNotifier implements RouterConfig { refreshListenable: refreshListenable); _routerDelegate = GoRouterDelegate( + routeInformationProvider: _routeInformationProvider, configuration: _routeConfiguration, errorPageBuilder: errorPageBuilder, errorBuilder: errorBuilder, diff --git a/packages/go_router/lib/src/state.dart b/packages/go_router/lib/src/state.dart index 52415f0a5aef..6fdf7a687fcc 100644 --- a/packages/go_router/lib/src/state.dart +++ b/packages/go_router/lib/src/state.dart @@ -316,14 +316,12 @@ class StatefulShellRouteState { /// Gets the [Widget]s representing each of the shell branches. /// /// The Widget returned from this method contains the [Navigator]s of the - /// branches. Note that the Widget for a particular branch may be null if the - /// branch hasn't been visited yet. Also note that the Widgets returned by this - /// method should only be added to the widget tree if using a custom - /// branch container Widget implementation, where the child parameter in the - /// [ShellRouteBuilder] of the [StatefulShellRoute] is ignored (i.e. not added - /// to the widget tree). + /// branches. Note that the Widgets returned by this method should only be + /// added to the widget tree if using a custom branch container Widget + /// implementation, where the child parameter in the [ShellRouteBuilder] of + /// the [StatefulShellRoute] is ignored (i.e. not added to the widget tree). /// See [StatefulShellBranchState.child]. - List get children => + List get children => branchStates.map((StatefulShellBranchState e) => e.child).toList(); /// Navigate to the current location of the shell navigator with the provided @@ -402,7 +400,7 @@ class StatefulShellBranchState { /// Constructs a [StatefulShellBranchState]. const StatefulShellBranchState({ required this.branch, - this.child, + required this.child, RouteMatchList? matchList, }) : _matchList = matchList; @@ -422,12 +420,11 @@ class StatefulShellBranchState { /// The [Widget] representing this route branch in a [StatefulShellRoute]. /// /// The Widget returned from this method contains the [Navigator] of the - /// branch. This field may be null until this route branch has been navigated - /// to at least once. Note that the Widget returned by this method should only + /// branch. Note that the Widget returned by this method should only /// be added to the widget tree if using a custom branch container Widget /// implementation, where the child parameter in the [ShellRouteBuilder] of /// the [StatefulShellRoute] is ignored (i.e. not added to the widget tree). - final Widget? child; + final Widget child; /// The current navigation stack for the branch. final RouteMatchList? _matchList; @@ -448,3 +445,9 @@ class StatefulShellBranchState { @override int get hashCode => Object.hash(branch, child, _matchList); } + +/// Helper extension on [StatefulShellBranchState], for internal use. +extension StatefulShellBranchStateHelper on StatefulShellBranchState { + /// The current navigation stack for the branch. + RouteMatchList? get matchList => _matchList; +} diff --git a/packages/go_router/test/builder_test.dart b/packages/go_router/test/builder_test.dart index ee7a8312ea1c..106f9b3a40f2 100644 --- a/packages/go_router/test/builder_test.dart +++ b/packages/go_router/test/builder_test.dart @@ -77,12 +77,12 @@ void main() { ); final RouteMatchList matches = RouteMatchList( - [ - _createRouteMatch(config.routes.first, '/'), - _createRouteMatch(config.routes.first.routes.first, '/'), - ], - Uri.parse('/'), - {}, + [ + _createRouteMatch(config.routes.first, '/'), + _createRouteMatch(config.routes.first.routes.first, '/'), + ], + Uri.parse('/'), + {}, ); await tester.pumpWidget( From 89b82c5a0c3af1151d2ea2b60daee1478167298d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20Lo=CC=88fstrand?= Date: Wed, 30 Nov 2022 14:30:55 +0100 Subject: [PATCH 057/112] Renamed StatefulShellRoute example file to stateful_shell_route.dart --- packages/go_router/example/README.md | 2 +- ...tateful_nested_navigation.dart => stateful_shell_route.dart} | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename packages/go_router/example/lib/{stateful_nested_navigation.dart => stateful_shell_route.dart} (100%) diff --git a/packages/go_router/example/README.md b/packages/go_router/example/README.md index 4649b44458b4..2f555a5b48e0 100644 --- a/packages/go_router/example/README.md +++ b/packages/go_router/example/README.md @@ -30,7 +30,7 @@ An example to demonstrate how to use redirect to handle a synchronous sign-in fl An example to demonstrate how to use handle a sign-in flow with a stream authentication service. -## [Stateful Nested Navigation](https://github.com/flutter/packages/blob/main/packages/go_router/example/lib/stateful_nested_navigation.dart) +## [Stateful Nested Navigation](https://github.com/flutter/packages/blob/main/packages/go_router/example/lib/stateful_shell_route.dart) `flutter run lib/stateful_nested_navigation.dart` An example to demonstrate how to use a `StatefulShellRoute` to create stateful nested navigation, with a diff --git a/packages/go_router/example/lib/stateful_nested_navigation.dart b/packages/go_router/example/lib/stateful_shell_route.dart similarity index 100% rename from packages/go_router/example/lib/stateful_nested_navigation.dart rename to packages/go_router/example/lib/stateful_shell_route.dart From 7392264b68070801635090f7e62cb66dca3e6e09 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20Lo=CC=88fstrand?= Date: Thu, 1 Dec 2022 19:22:25 +0100 Subject: [PATCH 058/112] Reduced unnecessary rebuilds of child Widgets of StatefulShellRoute, by caching branch Navigators. Added equality and hashcode to RouteMatchList and RouteMatch. Update stateful_shell_route.dart example with support (AppRouterProvider) for obtaining a reference to the GoRouter, without causing rebuilds. --- .../example/lib/stateful_shell_route.dart | 55 +++++-- packages/go_router/lib/src/builder.dart | 25 +-- packages/go_router/lib/src/delegate.dart | 16 +- packages/go_router/lib/src/match.dart | 21 ++- packages/go_router/lib/src/matching.dart | 28 +++- .../src/misc/stateful_navigation_shell.dart | 98 ++++++++---- packages/go_router/lib/src/route.dart | 21 +++ packages/go_router/lib/src/state.dart | 17 +- packages/go_router/test/builder_test.dart | 20 +-- packages/go_router/test/go_router_test.dart | 145 +++++++++++++++++- 10 files changed, 367 insertions(+), 79 deletions(-) diff --git a/packages/go_router/example/lib/stateful_shell_route.dart b/packages/go_router/example/lib/stateful_shell_route.dart index fed800bfa35d..b0dd6d6d2a7c 100644 --- a/packages/go_router/example/lib/stateful_shell_route.dart +++ b/packages/go_router/example/lib/stateful_shell_route.dart @@ -9,7 +9,7 @@ import 'package:go_router/go_router.dart'; final List _bottomNavBranches = [ StatefulShellBranch(rootLocation: '/a', name: 'A'), StatefulShellBranch(rootLocation: '/b', name: 'B'), - StatefulShellBranch(rootLocations: ['/c1', '/c2'], name: 'C'), + StatefulShellBranch(rootLocations: const ['/c1', '/c2'], name: 'C'), /// To enable preloading of the root routes of the branches, pass true /// for the parameter preload of StatefulShellBranch. @@ -35,6 +35,37 @@ void main() { runApp(NestedTabNavigationExampleApp()); } +/// InheritedWidget that provides a reference to the GoRouter in the widget +/// tree, without creating a dependency that triggers rebuilds. +/// +/// Simply use AppRouterProvider.of as an alternative to GoRouter.of, to get a +/// reference to the GoRouter, that doesn't cause rebuilds every time GoRouter +/// (or rather it's current location) changes. +class AppRouterProvider extends InheritedWidget { + /// Constructs an [AppRouterProvider]. + const AppRouterProvider({ + required Widget child, + required this.goRouter, + Key? key, + }) : super(child: child, key: key); + + /// The [GoRouter] instance used for this application. + final GoRouter goRouter; + + @override + bool updateShouldNotify(covariant AppRouterProvider oldWidget) { + return false; + } + + /// Find the current GoRouter in the widget tree. + static GoRouter of(BuildContext context) { + final AppRouterProvider? inherited = + context.dependOnInheritedWidgetOfExactType(); + assert(inherited != null, 'No GoRouter found in context'); + return inherited!.goRouter; + } +} + /// An example demonstrating how to use nested navigators class NestedTabNavigationExampleApp extends StatelessWidget { /// Creates a NestedTabNavigationExampleApp @@ -161,12 +192,15 @@ class NestedTabNavigationExampleApp extends StatelessWidget { @override Widget build(BuildContext context) { - return MaterialApp.router( - title: 'Flutter Demo', - theme: ThemeData( - primarySwatch: Colors.blue, + return AppRouterProvider( + goRouter: _router, + child: MaterialApp.router( + title: 'Flutter Demo', + theme: ThemeData( + primarySwatch: Colors.blue, + ), + routerConfig: _router, ), - routerConfig: _router, ); } } @@ -237,7 +271,8 @@ class RootScreen extends StatelessWidget { const Padding(padding: EdgeInsets.all(4)), TextButton( onPressed: () { - GoRouter.of(context).go(detailsPath, extra: '$label-XYZ'); + AppRouterProvider.of(context) + .go(detailsPath, extra: '$label-XYZ'); }, child: const Text('View details'), ), @@ -245,7 +280,7 @@ class RootScreen extends StatelessWidget { if (secondDetailsPath != null) TextButton( onPressed: () { - GoRouter.of(context).go(secondDetailsPath!); + AppRouterProvider.of(context).go(secondDetailsPath!); }, child: const Text('View more details'), ), @@ -332,7 +367,7 @@ class DetailsScreenState extends State { const Padding(padding: EdgeInsets.all(16)), TextButton( onPressed: () { - GoRouter.of(context).pop(); + AppRouterProvider.of(context).pop(); }, child: const Text('< Back', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 18)), @@ -409,7 +444,7 @@ class TabScreen extends StatelessWidget { if (detailsPath != null) TextButton( onPressed: () { - GoRouter.of(context).go(detailsPath!); + AppRouterProvider.of(context).go(detailsPath!); }, child: const Text('View details'), ), diff --git a/packages/go_router/lib/src/builder.dart b/packages/go_router/lib/src/builder.dart index 059258cb9eec..59387bc56f0c 100644 --- a/packages/go_router/lib/src/builder.dart +++ b/packages/go_router/lib/src/builder.dart @@ -206,19 +206,22 @@ class RouteBuilder { route, branches); }()); - assert( - branch != null, - 'Shell routes must always provide a navigator key for its immediate ' - 'sub-routes'); + if (branch == null) { + throw _RouteBuilderError('Shell routes must always provide a ' + 'navigator key for its immediate sub-routes'); + } // The key to provide to the shell route's Navigator. - shellNavigatorKey = branch!.navigatorKey; + shellNavigatorKey = branch.navigatorKey; buildRecursive(shellNavigatorKey); - child = _buildNavigator( - pop, keyToPages[shellNavigatorKey]!, shellNavigatorKey, - restorationScopeId: branch.restorationScopeId); + Navigator buildBranchNavigator() { + return _buildNavigator( + pop, keyToPages[shellNavigatorKey]!, shellNavigatorKey, + restorationScopeId: branch.restorationScopeId); + } + child = _buildStatefulNavigationShell(route, state, branches, branch, - child as Navigator, matchList, pop, registry); + buildBranchNavigator, matchList, pop, registry); } else if (route is ShellRoute) { // The key to provide to the shell route's Navigator. shellNavigatorKey = route.navigatorKey; @@ -269,7 +272,7 @@ class RouteBuilder { GoRouterState shellRouterState, List branches, StatefulShellBranch currentBranch, - Navigator currentNavigator, + BranchNavigatorBuilder currentNavigatorBuilder, RouteMatchList currentMatchList, VoidCallback pop, Map, GoRouterState> registry, @@ -280,7 +283,7 @@ class RouteBuilder { shellGoRouterState: shellRouterState, branches: branches, currentBranch: currentBranch, - currentNavigator: currentNavigator, + currentNavigatorBuilder: currentNavigatorBuilder, currentMatchList: currentMatchList, branchNavigatorBuilder: (BuildContext context, RouteMatchList matchList, diff --git a/packages/go_router/lib/src/delegate.dart b/packages/go_router/lib/src/delegate.dart index 22b8866efdf5..f72aa4c19674 100644 --- a/packages/go_router/lib/src/delegate.dart +++ b/packages/go_router/lib/src/delegate.dart @@ -219,7 +219,7 @@ class GoRouterDelegate extends RouterDelegate // TODO(chunhtai): Removes this once imperative API no longer insert route match. class ImperativeRouteMatch extends RouteMatch { /// Constructor for [ImperativeRouteMatch]. - ImperativeRouteMatch({ + const ImperativeRouteMatch({ required super.route, required super.subloc, required super.extra, @@ -230,4 +230,18 @@ class ImperativeRouteMatch extends RouteMatch { /// The matches that produces this route match. final RouteMatchList matches; + + @override + bool operator ==(Object other) { + if (identical(other, this)) { + return true; + } + if (other is! ImperativeRouteMatch) { + return false; + } + return super == this && other.matches == matches; + } + + @override + int get hashCode => Object.hash(super.hashCode, matches); } diff --git a/packages/go_router/lib/src/match.dart b/packages/go_router/lib/src/match.dart index 51420f538e41..f9c133041a7f 100644 --- a/packages/go_router/lib/src/match.dart +++ b/packages/go_router/lib/src/match.dart @@ -10,9 +10,10 @@ import 'path_utils.dart'; import 'route.dart'; /// An instance of a GoRoute plus information about the current location. +@immutable class RouteMatch { /// Constructor for [RouteMatch]. - RouteMatch({ + const RouteMatch({ required this.route, required this.subloc, required this.extra, @@ -75,4 +76,22 @@ class RouteMatch { /// Optional value key of type string, to hold a unique reference to a page. final ValueKey pageKey; + + @override + bool operator ==(Object other) { + if (identical(other, this)) { + return true; + } + if (other is! RouteMatch) { + return false; + } + return other.route == route && + other.subloc == subloc && + other.extra == extra && + other.error == error && + other.pageKey == pageKey; + } + + @override + int get hashCode => Object.hash(route, subloc, extra, error, pageKey); } diff --git a/packages/go_router/lib/src/matching.dart b/packages/go_router/lib/src/matching.dart index 345dc3f53737..4ab7c4257b26 100644 --- a/packages/go_router/lib/src/matching.dart +++ b/packages/go_router/lib/src/matching.dart @@ -2,6 +2,7 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'package:flutter/foundation.dart'; import 'package:flutter/widgets.dart'; import 'configuration.dart'; @@ -46,15 +47,22 @@ class RouteMatcher { } /// The list of [RouteMatch] objects. +@immutable class RouteMatchList { /// RouteMatchList constructor. RouteMatchList(List matches, this.uri, this.pathParameters) : _matches = matches, fullpath = _generateFullPath(matches); + /// Creates a clone of this RouteMatchList, that can be modified independently + /// of the original. + RouteMatchList clone() { + return RouteMatchList(matches.toList(), uri, pathParameters); + } + /// Constructs an empty matches object. - static RouteMatchList empty = - RouteMatchList([], Uri.parse(''), const {}); + static RouteMatchList empty = RouteMatchList( + const [], Uri.parse(''), const {}); static String _generateFullPath(List matches) { final StringBuffer buffer = StringBuffer(); @@ -136,6 +144,22 @@ class RouteMatchList { /// Returns the error that this match intends to display. Exception? get error => matches.first.error; + + @override + bool operator ==(Object other) { + if (identical(other, this)) { + return true; + } + if (other is! RouteMatchList) { + return false; + } + return listEquals(other.matches, matches) && + other.uri == uri && + other.pathParameters == pathParameters; + } + + @override + int get hashCode => Object.hash(matches, uri, pathParameters); } /// An error that occurred during matching. diff --git a/packages/go_router/lib/src/misc/stateful_navigation_shell.dart b/packages/go_router/lib/src/misc/stateful_navigation_shell.dart index 567c901dfb93..ca0a98af4ec8 100644 --- a/packages/go_router/lib/src/misc/stateful_navigation_shell.dart +++ b/packages/go_router/lib/src/misc/stateful_navigation_shell.dart @@ -32,8 +32,11 @@ class InheritedStatefulNavigationShell extends InheritedWidget { } } -/// Builder function for preloading a route branch navigator -typedef ShellRouteBranchNavigatorPreloadBuilder = Navigator Function( +/// Builder function for Navigator of the current branch. +typedef BranchNavigatorBuilder = Navigator Function(); + +/// Builder function for preloading a route branch navigator. +typedef BranchNavigatorPreloadBuilder = Navigator Function( BuildContext context, RouteMatchList navigatorMatchList, int startIndex, @@ -64,7 +67,7 @@ class StatefulNavigationShell extends StatefulWidget { required this.shellGoRouterState, required this.branches, required this.currentBranch, - required this.currentNavigator, + required this.currentNavigatorBuilder, required this.currentMatchList, required this.branchNavigatorBuilder, super.key, @@ -85,14 +88,14 @@ class StatefulNavigationShell extends StatefulWidget { /// The [StatefulShellBranch] for the current location final StatefulShellBranch currentBranch; - /// The navigator for the currently active route branch - final Navigator currentNavigator; + /// The builder for the navigator of the currently active route branch + final BranchNavigatorBuilder currentNavigatorBuilder; /// The RouteMatchList for the current location final RouteMatchList currentMatchList; /// Builder for route branch navigators (used for preloading). - final ShellRouteBranchNavigatorPreloadBuilder branchNavigatorBuilder; + final BranchNavigatorPreloadBuilder branchNavigatorBuilder; @override State createState() => StatefulNavigationShellState(); @@ -108,6 +111,10 @@ class StatefulNavigationShellState extends State { return _navigatorCache[branch.navigatorKey]; } + void _setNavigatorForBranch(StatefulShellBranch branch, Navigator navigator) { + _navigatorCache[branch.navigatorKey] = navigator; + } + int _findCurrentIndex() { final int index = widget.branches.indexWhere((StatefulShellBranch e) => e.navigatorKey == widget.currentBranch.navigatorKey); @@ -149,18 +156,19 @@ class StatefulNavigationShellState extends State { .indexWhere((RouteMatch e) => e.route == widget.shellRoute); // Keep only the routes from and below the root route in the match list and // use that to build the Navigator for the branch - Navigator? navigator; + BranchNavigatorBuilder? navigatorBuilder; if (shellRouteIndex >= 0 && shellRouteIndex < (matchList.matches.length - 1)) { - navigator = widget.branchNavigatorBuilder( - context, - matchList, - shellRouteIndex + 1, - branch.navigatorKey, - branch.restorationScopeId); + navigatorBuilder = () => widget.branchNavigatorBuilder( + context, + matchList, + shellRouteIndex + 1, + branch.navigatorKey, + branch.restorationScopeId, + ); } return _copyStatefulShellBranchState(branchState, - navigator: navigator, matchList: matchList); + navigatorBuilder: navigatorBuilder, matchList: matchList); } return routeMatchList.then(createBranchNavigator); @@ -174,6 +182,8 @@ class StatefulNavigationShellState extends State { final List newStates = []; + // Build a new list of the current StatefulShellBranchStates, with an + // updated state for the current branch etc. for (final StatefulShellBranch branch in branches) { if (branch.navigatorKey == branchState.navigatorKey) { newStates.add(branchState); @@ -184,6 +194,11 @@ class StatefulNavigationShellState extends State { } } + // Remove any obsolete cached Navigators + final Set validKeys = + branches.map((StatefulShellBranch e) => e.navigatorKey).toSet(); + _navigatorCache.removeWhere((Key key, _) => !validKeys.contains(key)); + _routeState = _routeState.copy( branchStates: newStates, currentIndex: currentIndex, @@ -194,7 +209,7 @@ class StatefulNavigationShellState extends State { final List states = _routeState.branchStates; for (StatefulShellBranchState state in states) { if (state.branch.preload && !state.preloading) { - state = _copyStatefulShellBranchState(state, preloading: true); + state = _copyStatefulShellBranchState(state, loaded: true); _preloadBranch(state).then((StatefulShellBranchState navigatorState) { setState(() { _updateRouteBranchState(navigatorState); @@ -208,10 +223,22 @@ class StatefulNavigationShellState extends State { final int index = _findCurrentIndex(); final StatefulShellBranch branch = widget.currentBranch; + // Update or create a new StatefulShellBranchState for the current branch + // (i.e. the arguments currently provided to the Widget). + StatefulShellBranchState? existingState = _routeState.branchStates + .firstWhereOrNull((StatefulShellBranchState e) => e.branch == branch); + if (existingState != null) { + existingState = _copyStatefulShellBranchState(existingState, + navigatorBuilder: widget.currentNavigatorBuilder, + matchList: widget.currentMatchList); + } else { + existingState = _createStatefulShellBranchState(branch, + navigatorBuilder: widget.currentNavigatorBuilder, + matchList: widget.currentMatchList); + } + _updateRouteBranchState( - _createStatefulShellBranchState(branch, - navigator: widget.currentNavigator, - matchList: widget.currentMatchList), + existingState, currentIndex: index, ); @@ -228,26 +255,31 @@ class StatefulNavigationShellState extends State { StatefulShellBranchState _copyStatefulShellBranchState( StatefulShellBranchState branchState, - {Navigator? navigator, + {BranchNavigatorBuilder? navigatorBuilder, RouteMatchList? matchList, - bool preloading = false}) { - if (navigator != null) { - _navigatorCache[branchState.navigatorKey] = navigator; + bool? loaded}) { + if (navigatorBuilder != null) { + final Navigator? existingNav = _navigatorForBranch(branchState.branch); + if (existingNav == null || branchState.matchList != matchList) { + _setNavigatorForBranch(branchState.branch, navigatorBuilder()); + } } final _BranchNavigatorProxy branchWidget = branchState.child as _BranchNavigatorProxy; + final bool isLoaded = + loaded ?? _navigatorForBranch(branchState.branch) != null; return branchState.copy( - child: branchWidget.copy(preloading: preloading), - matchList: matchList, + child: branchWidget.copy(loaded: isLoaded), + matchList: matchList?.clone(), ); } StatefulShellBranchState _createStatefulShellBranchState( StatefulShellBranch branch, - {Navigator? navigator, + {BranchNavigatorBuilder? navigatorBuilder, RouteMatchList? matchList}) { - if (navigator != null) { - _navigatorCache[branch.navigatorKey] = navigator; + if (navigatorBuilder != null) { + _setNavigatorForBranch(branch, navigatorBuilder()); } return StatefulShellBranchState( branch: branch, @@ -255,7 +287,7 @@ class StatefulNavigationShellState extends State { branch: branch, navigatorForBranch: _navigatorForBranch, ), - matchList: matchList, + matchList: matchList?.clone(), ); } @@ -317,13 +349,13 @@ class _BranchNavigatorProxy extends StatelessWidget { const _BranchNavigatorProxy( {required this.branch, required this.navigatorForBranch, - this.preloading = false, + this.loaded = false, super.key}); - _BranchNavigatorProxy copy({bool preloading = false}) { + _BranchNavigatorProxy copy({bool? loaded}) { return _BranchNavigatorProxy( branch: branch, - preloading: preloading, + loaded: loaded ?? this.loaded, navigatorForBranch: navigatorForBranch, key: key, ); @@ -331,7 +363,7 @@ class _BranchNavigatorProxy extends StatelessWidget { final StatefulShellBranch branch; final _NavigatorForBranch navigatorForBranch; - final bool preloading; + final bool loaded; @override Widget build(BuildContext context) { @@ -393,5 +425,5 @@ extension StatefulShellBranchResolver on StatefulShellRoute { extension _StatefulShellBranchStateHelper on StatefulShellBranchState { GlobalKey get navigatorKey => branch.navigatorKey; - bool get preloading => (child as _BranchNavigatorProxy).preloading; + bool get preloading => (child as _BranchNavigatorProxy).loaded; } diff --git a/packages/go_router/lib/src/route.dart b/packages/go_router/lib/src/route.dart index ea6e1c007c01..6e92606184b6 100644 --- a/packages/go_router/lib/src/route.dart +++ b/packages/go_router/lib/src/route.dart @@ -3,6 +3,7 @@ // found in the LICENSE file. import 'package:collection/collection.dart'; +import 'package:flutter/foundation.dart'; import 'package:flutter/widgets.dart'; import 'package:meta/meta.dart'; @@ -705,6 +706,7 @@ class StatefulShellRoute extends ShellRouteBase { /// /// A [navigatorKey] is optional, but can be useful to provide in case you need /// to use the [Navigator] created for this branch elsewhere. +@immutable class StatefulShellBranch { /// Constructs a [StatefulShellBranch]. StatefulShellBranch({ @@ -761,4 +763,23 @@ class StatefulShellBranch { /// in the Widget tree. static StatefulShellBranchState of(BuildContext context) => StatefulShellRoute.of(context).currentBranchState; + + @override + bool operator ==(Object other) { + if (identical(other, this)) { + return true; + } + if (other is! StatefulShellBranch) { + return false; + } + return other.navigatorKey == navigatorKey && + listEquals(other.rootLocations, rootLocations) && + other.name == name && + other.preload == preload && + other.restorationScopeId == restorationScopeId; + } + + @override + int get hashCode => Object.hash( + navigatorKey, rootLocations, name, preload, restorationScopeId); } diff --git a/packages/go_router/lib/src/state.dart b/packages/go_router/lib/src/state.dart index 6fdf7a687fcc..8c6678313b30 100644 --- a/packages/go_router/lib/src/state.dart +++ b/packages/go_router/lib/src/state.dart @@ -341,16 +341,19 @@ class StatefulShellRouteState { assert(navigatorKey != null || name != null || index != null); assert([navigatorKey, name, index].whereNotNull().length == 1); - final StatefulShellBranchState state; + final StatefulShellBranchState? state; if (navigatorKey != null) { - state = branchStates.firstWhere((StatefulShellBranchState e) => + state = branchStates.firstWhereOrNull((StatefulShellBranchState e) => e.branch.navigatorKey == navigatorKey); - assert(state != null, - 'Unable to find ShellNavigator with key $navigatorKey'); + if (state == null) { + throw GoError('Unable to find ShellNavigator with key $navigatorKey'); + } } else if (name != null) { - state = branchStates - .firstWhere((StatefulShellBranchState e) => e.branch.name == name); - assert(state != null, 'Unable to find ShellNavigator with name "$name"'); + state = branchStates.firstWhereOrNull( + (StatefulShellBranchState e) => e.branch.name == name); + if (state == null) { + throw GoError('Unable to find ShellNavigator with name "$name"'); + } } else { state = branchStates[index!]; } diff --git a/packages/go_router/test/builder_test.dart b/packages/go_router/test/builder_test.dart index 106f9b3a40f2..4d2bc0f75be9 100644 --- a/packages/go_router/test/builder_test.dart +++ b/packages/go_router/test/builder_test.dart @@ -82,7 +82,7 @@ void main() { _createRouteMatch(config.routes.first.routes.first, '/'), ], Uri.parse('/'), - {}, + const {}, ); await tester.pumpWidget( @@ -126,7 +126,7 @@ void main() { _createRouteMatch(config.routes.first.routes.first, '/nested'), ], Uri.parse('/nested'), - {}); + const {}); await tester.pumpWidget( _BuilderTestWidget( @@ -174,7 +174,7 @@ void main() { _createRouteMatch(config.routes.first.routes.first, '/b'), ], Uri.parse('/b'), - {}); + const {}); await tester.pumpWidget( _BuilderTestWidget( @@ -207,7 +207,7 @@ void main() { }, branches: [ StatefulShellBranch(rootLocation: '/a'), - StatefulShellBranch(rootLocations: ['/a', '/b']), + StatefulShellBranch(rootLocations: const ['/a', '/b']), ]), ], redirectLimit: 10, @@ -221,7 +221,7 @@ void main() { _createRouteMatch(config.routes.first.routes.first, '/b'), ], Uri.parse('/b'), - {}); + const {}); await tester.pumpWidget( _BuilderTestWidget( @@ -269,7 +269,7 @@ void main() { _createRouteMatch(config.routes.first.routes.first, '/b'), ], Uri.parse('/b'), - {}); + const {}); await tester.pumpWidget( _BuilderTestWidget( @@ -311,7 +311,7 @@ void main() { ), ], Uri.parse('/'), - {}); + const {}); await tester.pumpWidget( _BuilderTestWidget( @@ -373,7 +373,7 @@ void main() { ), ], Uri.parse('/details'), - {}); + const {}); await tester.pumpWidget( _BuilderTestWidget( @@ -441,7 +441,7 @@ void main() { ), ], Uri.parse('/a/details'), - {}); + const {}); await tester.pumpWidget( _BuilderTestWidget( @@ -493,7 +493,7 @@ void main() { _createRouteMatch(config.routes.first.routes.first, '/a'), ], Uri.parse('/b'), - {}); + const {}); await tester.pumpWidget( _BuilderTestWidget( diff --git a/packages/go_router/test/go_router_test.dart b/packages/go_router/test/go_router_test.dart index 554efd5eb211..1c045f905d2a 100644 --- a/packages/go_router/test/go_router_test.dart +++ b/packages/go_router/test/go_router_test.dart @@ -12,6 +12,7 @@ import 'package:go_router/go_router.dart'; import 'package:go_router/src/delegate.dart'; import 'package:go_router/src/match.dart'; import 'package:go_router/src/matching.dart'; +import 'package:go_router/src/misc/errors.dart'; import 'package:logging/logging.dart'; import 'test_helpers.dart'; @@ -2629,10 +2630,14 @@ void main() { GlobalKey(); final GlobalKey statefulWidgetKey = GlobalKey(); + StatefulShellRouteState? routeState; final List routes = [ StatefulShellRoute( - builder: (_, __, Widget child) => child, + builder: (BuildContext context, GoRouterState state, Widget child) { + routeState = StatefulShellRoute.of(context); + return child; + }, routes: [ GoRoute( path: '/a', @@ -2669,17 +2674,17 @@ void main() { expect(find.text('Screen A Detail'), findsOneWidget); expect(find.text('Screen B'), findsNothing); - router.go('/b'); + routeState!.goBranch(index: 1); await tester.pumpAndSettle(); expect(find.text('Screen A'), findsNothing); expect(find.text('Screen A Detail'), findsNothing); expect(find.text('Screen B'), findsOneWidget); - router.go('/a/detailA'); + routeState!.goBranch(index: 0); await tester.pumpAndSettle(); expect(statefulWidgetKey.currentState?.counter, equals(1)); - router.pop(); + routeState!.goBranch(index: 0, resetLocation: true); await tester.pumpAndSettle(); expect(find.text('Screen A'), findsOneWidget); expect(find.text('Screen A Detail'), findsNothing); @@ -3129,6 +3134,138 @@ void main() { expect(find.text('Screen A'), findsOneWidget); expect(find.text('Screen A Detail'), findsNothing); }); + + testWidgets( + 'Dynamic branches are created, removed and updated correctly in ' + 'a StatefulShellRoute', (WidgetTester tester) async { + final GlobalKey rootNavigatorKey = + GlobalKey(debugLabel: 'root'); + final GlobalKey branch0 = + GlobalKey(debugLabel: 'branch0'); + final GlobalKey branch1 = + GlobalKey(debugLabel: 'branch1'); + final GlobalKey branch2 = + GlobalKey(debugLabel: 'branch2'); + + StatefulShellRouteState? routeState; + final ValueNotifier scenario = ValueNotifier(1); + + final GoRouter router = GoRouter( + navigatorKey: rootNavigatorKey, + initialLocation: '/a/0', + routes: [ + StatefulShellRoute( + builder: (BuildContext context, GoRouterState state, Widget child) { + routeState = StatefulShellRoute.of(context); + return child; + }, + branchBuilder: (_, __) => [ + StatefulShellBranch(rootLocation: '/a/0', navigatorKey: branch0), + if (scenario.value == 2) ...[ + StatefulShellBranch( + rootLocation: '/a/1', navigatorKey: branch1), + StatefulShellBranch( + rootLocation: '/a/2', navigatorKey: branch2), + ], + if (scenario.value == 3) ...[ + StatefulShellBranch( + rootLocation: '/a/1', + name: 'branch1', + navigatorKey: branch1), + StatefulShellBranch( + rootLocation: '/a/2', navigatorKey: branch2), + ], + if (scenario.value == 4) + StatefulShellBranch( + rootLocation: '/a/1', navigatorKey: branch1), + ], + routes: [ + GoRoute( + path: '/a/:id', + builder: (BuildContext context, GoRouterState state) { + return Text('a-${state.params['id']}'); + }, + routes: [ + GoRoute( + path: 'detail', + builder: (BuildContext context, GoRouterState state) { + return Text('a-detail-${state.params['id']}'); + }, + ), + ]), + ], + ), + ], + errorBuilder: (BuildContext context, GoRouterState state) => + Text('error:${GoRouter.of(context).location}'), + ); + + await tester.pumpWidget( + ValueListenableBuilder( + valueListenable: scenario, + builder: (_, __, ___) { + return MaterialApp.router(routerConfig: router); + }), + ); + + expect(find.text('a-0'), findsOneWidget); + expect(find.text('a-detail-0'), findsNothing); + expect(find.byKey(branch0), findsOneWidget); + + router.go('/a/0/detail'); + await tester.pumpAndSettle(); + expect(find.text('a-0'), findsNothing); + expect(find.text('a-detail-0'), findsOneWidget); + + router.go('/a/1'); + await tester.pumpAndSettle(); + expect(find.text('a-1'), findsNothing); + expect(find.text('error:/a/1'), findsOneWidget); + + scenario.value = 2; + await tester.pumpAndSettle(); + routeState!.goBranch(navigatorKey: branch2); + await tester.pumpAndSettle(); + expect(find.text('a-0'), findsNothing); + expect(find.text('a-1'), findsNothing); + expect(find.text('a-2'), findsOneWidget); + + expect(() { + // Name 'branch1' hasn't yet been assigned, so this should fail + routeState!.goBranch(name: 'branch1'); + }, throwsA(isA())); + scenario.value = 3; + await tester.pumpAndSettle(); + routeState!.goBranch(name: 'branch1'); + await tester.pumpAndSettle(); + expect(find.text('a-0'), findsNothing); + expect(find.text('a-1'), findsOneWidget); + expect(find.text('a-2'), findsNothing); + expect(routeState!.branchStates.length, 3); + + router.go('/a/2/detail'); + await tester.pumpAndSettle(); + expect(find.text('a-2'), findsNothing); + expect(find.text('a-detail-2'), findsOneWidget); + routeState!.goBranch(name: 'branch1'); + await tester.pumpAndSettle(); + + // Test removal of branch2 + scenario.value = 4; + await tester.pumpAndSettle(); + expect(routeState!.branchStates.length, 2); + expect(() { + routeState!.goBranch(navigatorKey: branch2); + }, throwsA(isA())); + + // Test that state of branch2 is forgotten (not restored) after removal + scenario.value = 3; + await tester.pumpAndSettle(); + routeState!.goBranch(navigatorKey: branch2); + await tester.pumpAndSettle(); + expect(find.text('a-2'), findsOneWidget); + expect(find.text('a-detail-2'), findsNothing); + }); }); group('Imperative navigation', () { From 845c0529aa97c33e5d9be19e3401c25fff3ebb7f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20Lo=CC=88fstrand?= Date: Fri, 2 Dec 2022 23:08:53 +0100 Subject: [PATCH 059/112] Minor refactoring - moved StatefulShellBranchResolver to route.dart. --- packages/go_router/lib/src/delegate.dart | 1 - .../src/misc/stateful_navigation_shell.dart | 19 --------------- packages/go_router/lib/src/route.dart | 23 +++++++++++++++++++ 3 files changed, 23 insertions(+), 20 deletions(-) diff --git a/packages/go_router/lib/src/delegate.dart b/packages/go_router/lib/src/delegate.dart index b646ca27b960..72a19ef8f05d 100644 --- a/packages/go_router/lib/src/delegate.dart +++ b/packages/go_router/lib/src/delegate.dart @@ -13,7 +13,6 @@ import 'information_provider.dart'; import 'match.dart'; import 'matching.dart'; import 'misc/errors.dart'; -import 'misc/stateful_navigation_shell.dart'; import 'typedefs.dart'; /// GoRouter implementation of [RouterDelegate]. diff --git a/packages/go_router/lib/src/misc/stateful_navigation_shell.dart b/packages/go_router/lib/src/misc/stateful_navigation_shell.dart index ca0a98af4ec8..20289055063f 100644 --- a/packages/go_router/lib/src/misc/stateful_navigation_shell.dart +++ b/packages/go_router/lib/src/misc/stateful_navigation_shell.dart @@ -404,25 +404,6 @@ class _IndexedStackedRouteBranchContainer extends StatelessWidget { } } -/// StatefulShellRoute extension that provides support for resolving the -/// current StatefulShellBranch. -extension StatefulShellBranchResolver on StatefulShellRoute { - static final Expando _shellBranchCache = - Expando(); - - /// The current StatefulShellBranch, previously resolved using [resolveBranch]. - StatefulShellBranch? get currentBranch => _shellBranchCache[this]; - - /// Resolves the current StatefulShellBranch, given the provided GoRouterState. - StatefulShellBranch? resolveBranch( - List branches, GoRouterState state) { - final StatefulShellBranch? branch = branches - .firstWhereOrNull((StatefulShellBranch e) => e.isBranchFor(state)); - _shellBranchCache[this] = branch; - return branch; - } -} - extension _StatefulShellBranchStateHelper on StatefulShellBranchState { GlobalKey get navigatorKey => branch.navigatorKey; bool get preloading => (child as _BranchNavigatorProxy).loaded; diff --git a/packages/go_router/lib/src/route.dart b/packages/go_router/lib/src/route.dart index 6e92606184b6..81911bf82222 100644 --- a/packages/go_router/lib/src/route.dart +++ b/packages/go_router/lib/src/route.dart @@ -783,3 +783,26 @@ class StatefulShellBranch { int get hashCode => Object.hash( navigatorKey, rootLocations, name, preload, restorationScopeId); } + +/// StatefulShellRoute extension that provides support for resolving the +/// current StatefulShellBranch. +/// +/// Should not be used directly, consider using [StatefulShellRoute.of] or +/// [StatefulShellBranch.of] to access [StatefulShellBranchState] for the +/// current context. +extension StatefulShellBranchResolver on StatefulShellRoute { + static final Expando _shellBranchCache = + Expando(); + + /// The current StatefulShellBranch, previously resolved using [resolveBranch]. + StatefulShellBranch? get currentBranch => _shellBranchCache[this]; + + /// Resolves the current StatefulShellBranch, given the provided GoRouterState. + StatefulShellBranch? resolveBranch( + List branches, GoRouterState state) { + final StatefulShellBranch? branch = branches + .firstWhereOrNull((StatefulShellBranch e) => e.isBranchFor(state)); + _shellBranchCache[this] = branch; + return branch; + } +} From 193a267e868813c7f723c96471a6d3446b724a64 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20Lo=CC=88fstrand?= Date: Thu, 8 Dec 2022 09:20:07 +0100 Subject: [PATCH 060/112] Fixed issue in StatefulShellBranch.isBranchFor (accidental use of incorrect field of GoRouterState). Added unit tests for StatefulShellBranch.isBranchFor as well as for StatefulShellRouteState.goBranch. Fixed typos in documentation. --- packages/go_router/lib/src/route.dart | 2 +- packages/go_router/lib/src/state.dart | 12 +-- packages/go_router/test/builder_test.dart | 58 +++++++++++ packages/go_router/test/go_router_test.dart | 101 ++++++++++++++++++++ 4 files changed, 166 insertions(+), 7 deletions(-) diff --git a/packages/go_router/lib/src/route.dart b/packages/go_router/lib/src/route.dart index 81911bf82222..4930472da99f 100644 --- a/packages/go_router/lib/src/route.dart +++ b/packages/go_router/lib/src/route.dart @@ -755,7 +755,7 @@ class StatefulShellBranch { /// GoRouterState. bool isBranchFor(GoRouterState state) { final String? match = rootLocations - .firstWhereOrNull((String e) => state.subloc.startsWith(e)); + .firstWhereOrNull((String e) => state.location.startsWith(e)); return match != null; } diff --git a/packages/go_router/lib/src/state.dart b/packages/go_router/lib/src/state.dart index 8c6678313b30..44910b27963d 100644 --- a/packages/go_router/lib/src/state.dart +++ b/packages/go_router/lib/src/state.dart @@ -295,13 +295,13 @@ class StatefulShellRouteState { /// [StatefulShellRoute]. final List branchStates; - /// The state associated with the current [ShellNavigator]. + /// The state associated with the current [StatefulShellBranch]. StatefulShellBranchState get currentBranchState => branchStates[currentIndex]; - /// The index of the currently active [ShellNavigator]. + /// The index of the currently active [StatefulShellBranch]. /// - /// Corresponds to the index of the ShellNavigator in the List returned from - /// shellNavigatorBuilder of [StatefulShellRoute]. + /// Corresponds to the index of the branch in the List returned from + /// branchBuilder of [StatefulShellRoute]. final int currentIndex; /// The Navigator key of the current navigator. @@ -346,13 +346,13 @@ class StatefulShellRouteState { state = branchStates.firstWhereOrNull((StatefulShellBranchState e) => e.branch.navigatorKey == navigatorKey); if (state == null) { - throw GoError('Unable to find ShellNavigator with key $navigatorKey'); + throw GoError('Unable to find branch with key $navigatorKey'); } } else if (name != null) { state = branchStates.firstWhereOrNull( (StatefulShellBranchState e) => e.branch.name == name); if (state == null) { - throw GoError('Unable to find ShellNavigator with name "$name"'); + throw GoError('Unable to find branch with name "$name"'); } } else { state = branchStates[index!]; diff --git a/packages/go_router/test/builder_test.dart b/packages/go_router/test/builder_test.dart index 0f3d6a272a5d..ac405478321b 100644 --- a/packages/go_router/test/builder_test.dart +++ b/packages/go_router/test/builder_test.dart @@ -139,6 +139,64 @@ void main() { expect(find.byKey(key), findsOneWidget); }); + testWidgets('Builds StatefulShellRoute as a sub-route', + (WidgetTester tester) async { + final GlobalKey key = + GlobalKey(debugLabel: 'key'); + late GoRoute root; + late StatefulShellRoute shell; + late GoRoute nested; + final RouteConfiguration config = RouteConfiguration( + routes: [ + root = GoRoute( + path: '/root', + builder: (BuildContext context, GoRouterState state) => + const Text('Root'), + routes: [ + shell = StatefulShellRoute( + builder: (_, __, Widget child) => child, + branches: [ + StatefulShellBranch( + rootLocation: '/root/nested', navigatorKey: key), + ], + routes: [ + nested = GoRoute( + path: 'nested', + builder: (BuildContext context, GoRouterState state) { + return _DetailsScreen(); + }, + ), + ], + ), + ], + ) + ], + redirectLimit: 10, + topRedirect: (_, __) => null, + navigatorKey: GlobalKey(), + ); + + final RouteMatchList matches = RouteMatchList( + [ + _createRouteMatch(root, '/root'), + _createRouteMatch(shell, 'nested'), + _createRouteMatch(nested, '/root/nested'), + ], + Uri.parse('/root/nested'), + const {}, + ); + + await tester.pumpWidget( + _BuilderTestWidget( + routeConfiguration: config, + matches: matches, + ), + ); + + expect(find.byType(_DetailsScreen), findsOneWidget); + expect(find.byKey(key), findsOneWidget); + }); + testWidgets( 'throws when a branch of a StatefulShellRoute has an incorrect ' 'defaultLocation', (WidgetTester tester) async { diff --git a/packages/go_router/test/go_router_test.dart b/packages/go_router/test/go_router_test.dart index 2dd1a87780e9..1fcd049a1acf 100644 --- a/packages/go_router/test/go_router_test.dart +++ b/packages/go_router/test/go_router_test.dart @@ -2623,6 +2623,107 @@ void main() { expect(find.text('Screen C'), findsNothing); }); + testWidgets( + 'Navigation with goBranch is correctly handled in StatefulShellRoute', + (WidgetTester tester) async { + final GlobalKey rootNavigatorKey = + GlobalKey(); + final GlobalKey branchANavigatorKey = + GlobalKey(); + final GlobalKey branchCNavigatorKey = + GlobalKey(); + final GlobalKey statefulWidgetKey = + GlobalKey(); + StatefulShellRouteState? routeState; + + final List routes = [ + StatefulShellRoute( + builder: (BuildContext context, GoRouterState state, Widget child) { + routeState = StatefulShellRoute.of(context); + return child; + }, + routes: [ + GoRoute( + path: '/a', + builder: (BuildContext context, GoRouterState state) => + const Text('Screen A'), + ), + GoRoute( + path: '/b', + builder: (BuildContext context, GoRouterState state) => + const Text('Screen B'), + ), + GoRoute( + path: '/c', + builder: (BuildContext context, GoRouterState state) => + const Text('Screen C'), + ), + GoRoute( + path: '/d', + builder: (BuildContext context, GoRouterState state) => + const Text('Screen D'), + ), + ], + branches: [ + StatefulShellBranch(rootLocation: '/a'), + StatefulShellBranch(rootLocation: '/b', name: 'B'), + StatefulShellBranch( + rootLocation: '/c', navigatorKey: branchCNavigatorKey), + StatefulShellBranch(rootLocation: '/d'), + ], + ), + ]; + + await createRouter(routes, tester, + initialLocation: '/a', navigatorKey: rootNavigatorKey); + statefulWidgetKey.currentState?.increment(); + expect(find.text('Screen A'), findsOneWidget); + expect(find.text('Screen B'), findsNothing); + expect(find.text('Screen C'), findsNothing); + expect(find.text('Screen D'), findsNothing); + + routeState!.goBranch(name: 'B'); + 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(navigatorKey: branchCNavigatorKey); + 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(index: 3); + await tester.pumpAndSettle(); + expect(find.text('Screen A'), findsNothing); + expect(find.text('Screen B'), findsNothing); + expect(find.text('Screen C'), findsNothing); + expect(find.text('Screen D'), findsOneWidget); + + expect(() { + // Verify that navigation without specifying name, key or index fails + routeState!.goBranch(); + }, throwsA(isAssertionError)); + + expect(() { + // Verify that navigation to unknown name fails + routeState!.goBranch(name: 'C'); + }, throwsA(isA())); + + expect(() { + // Verify that navigation to unknown name fails + routeState!.goBranch(navigatorKey: branchANavigatorKey); + }, throwsA(isA())); + + expect(() { + // Verify that navigation to unknown index fails + routeState!.goBranch(index: 4); + }, throwsA(isA())); + }); + testWidgets( 'Navigates to correct nested navigation tree in StatefulShellRoute ' 'and maintains state', (WidgetTester tester) async { From c562482e3000bf3fb02effc007fa58ade51290e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20Lo=CC=88fstrand?= Date: Thu, 8 Dec 2022 13:02:45 +0100 Subject: [PATCH 061/112] Reverted workaround regarding pop. Introduced UnmodifiableRouteMatchList (mostly due to mutability changes in RouteMatchList). --- packages/go_router/lib/src/builder.dart | 2 +- packages/go_router/lib/src/delegate.dart | 14 +---- packages/go_router/lib/src/matching.dart | 61 +++++++++++-------- .../src/misc/stateful_navigation_shell.dart | 19 +++--- packages/go_router/lib/src/router.dart | 1 - packages/go_router/lib/src/state.dart | 14 +++-- packages/go_router/test/go_router_test.dart | 16 ++++- 7 files changed, 70 insertions(+), 57 deletions(-) diff --git a/packages/go_router/lib/src/builder.dart b/packages/go_router/lib/src/builder.dart index 8561cc7395b0..3cb707d1e840 100644 --- a/packages/go_router/lib/src/builder.dart +++ b/packages/go_router/lib/src/builder.dart @@ -276,7 +276,7 @@ class RouteBuilder { branches: branches, currentBranch: currentBranch, currentNavigatorBuilder: currentNavigatorBuilder, - currentMatchList: currentMatchList, + currentMatchList: currentMatchList.unmodifiableRouteMatchList(), branchNavigatorBuilder: (BuildContext context, RouteMatchList matchList, int startIndex, diff --git a/packages/go_router/lib/src/delegate.dart b/packages/go_router/lib/src/delegate.dart index 010c411cb153..bc6863721af1 100644 --- a/packages/go_router/lib/src/delegate.dart +++ b/packages/go_router/lib/src/delegate.dart @@ -9,7 +9,6 @@ import 'package:flutter/widgets.dart'; import 'builder.dart'; import 'configuration.dart'; -import 'information_provider.dart'; import 'match.dart'; import 'matching.dart'; import 'misc/errors.dart'; @@ -21,7 +20,6 @@ class GoRouterDelegate extends RouterDelegate /// Constructor for GoRouter's implementation of the RouterDelegate base /// class. GoRouterDelegate({ - required GoRouteInformationProvider routeInformationProvider, required RouteConfiguration configuration, required GoRouterBuilderWithNav builderWithNav, required GoRouterPageBuilder? errorPageBuilder, @@ -29,8 +27,7 @@ class GoRouterDelegate extends RouterDelegate required List observers, required this.routerNeglect, String? restorationScopeId, - }) : _routeInformationProvider = routeInformationProvider, - _configuration = configuration, + }) : _configuration = configuration, builder = RouteBuilder( configuration: configuration, builderWithNav: builderWithNav, @@ -49,8 +46,6 @@ class GoRouterDelegate extends RouterDelegate RouteMatchList _matchList = RouteMatchList.empty; - late final GoRouteInformationProvider _routeInformationProvider; - /// Stores the number of times each route route has been pushed. /// /// This is used to generate a unique key for each route. @@ -134,7 +129,6 @@ class GoRouterDelegate extends RouterDelegate } bool _onPopPage(Route route, Object? result) { - final bool lastMatchIsImperative = _matchList.last is ImperativeRouteMatch; if (!route.didPop(result)) { return false; } @@ -144,12 +138,6 @@ class GoRouterDelegate extends RouterDelegate _debugAssertMatchListNotEmpty(); return true; }()); - if (!lastMatchIsImperative) { - // TODO(tolo): Temporary workaround (here and in RouteMatchList) to get expected pop behaviour - _routeInformationProvider.value = RouteInformation( - location: _matchList.lastMatchUri.toString(), - state: _matchList.last.extra); - } return true; } diff --git a/packages/go_router/lib/src/matching.dart b/packages/go_router/lib/src/matching.dart index aedaf0ca109f..5751e6cf3747 100644 --- a/packages/go_router/lib/src/matching.dart +++ b/packages/go_router/lib/src/matching.dart @@ -47,17 +47,15 @@ class RouteMatcher { } /// The list of [RouteMatch] objects. -@immutable class RouteMatchList { /// RouteMatchList constructor. RouteMatchList(List matches, this._uri, this.pathParameters) : _matches = matches, fullpath = _generateFullPath(matches); - /// Creates a clone of this RouteMatchList, that can be modified independently - /// of the original. - RouteMatchList clone() { - return RouteMatchList(matches.toList(), uri, pathParameters); + /// Creates an immutable clone of this RouteMatchList. + UnmodifiableRouteMatchList unmodifiableRouteMatchList() { + return UnmodifiableRouteMatchList.from(this); } /// Constructs an empty matches object. @@ -80,17 +78,6 @@ class RouteMatchList { return buffer.toString(); } - static String _addQueryParams( - String loc, Map queryParametersAll) { - final Uri uri = Uri.parse(loc); - assert(uri.queryParameters.isEmpty); - return Uri( - path: uri.path, - queryParameters: - queryParametersAll.isEmpty ? null : queryParametersAll) - .toString(); - } - final List _matches; /// the full path pattern that matches the uri. @@ -104,12 +91,6 @@ class RouteMatchList { Uri get uri => _uri; Uri _uri; - /// The uri of the last match. - Uri get lastMatchUri => _matches.isEmpty - ? Uri(queryParameters: uri.queryParametersAll) - : Uri.parse( - _addQueryParams(_matches.last.subloc, uri.queryParametersAll)); - /// Returns true if there are no matches. bool get isEmpty => _matches.isEmpty; @@ -148,22 +129,48 @@ class RouteMatchList { /// Returns the error that this match intends to display. Exception? get error => matches.first.error; +} + +/// Unmodifiable version of [RouteMatchList]. +@immutable +class UnmodifiableRouteMatchList { + /// UnmodifiableRouteMatchList constructor. + UnmodifiableRouteMatchList.from(RouteMatchList routeMatchList) + : _matches = List.unmodifiable(routeMatchList.matches), + _uri = routeMatchList.uri, + _pathParameters = + Map.unmodifiable(routeMatchList.pathParameters); + + /// Creates a new [RouteMatchList] from this UnmodifiableRouteMatchList. + RouteMatchList get routeMatchList => RouteMatchList( + List.from(_matches), + _uri, + Map.from(_pathParameters)); + + /// The route matches. + final List _matches; + + /// The uri of the current match. + final Uri _uri; + + /// Parameters for the matched route, URI-encoded. + final Map _pathParameters; @override bool operator ==(Object other) { if (identical(other, this)) { return true; } - if (other is! RouteMatchList) { + if (other is! UnmodifiableRouteMatchList) { return false; } - return listEquals(other.matches, matches) && - other.uri == uri && - other.pathParameters == pathParameters; + return listEquals(other._matches, _matches) && + other._uri == _uri && + other._pathParameters == _pathParameters; } @override - int get hashCode => Object.hash(matches, uri, pathParameters); + int get hashCode => Object.hash(_matches, _uri, _pathParameters); } /// An error that occurred during matching. diff --git a/packages/go_router/lib/src/misc/stateful_navigation_shell.dart b/packages/go_router/lib/src/misc/stateful_navigation_shell.dart index 20289055063f..6c348775ae17 100644 --- a/packages/go_router/lib/src/misc/stateful_navigation_shell.dart +++ b/packages/go_router/lib/src/misc/stateful_navigation_shell.dart @@ -92,7 +92,7 @@ class StatefulNavigationShell extends StatefulWidget { final BranchNavigatorBuilder currentNavigatorBuilder; /// The RouteMatchList for the current location - final RouteMatchList currentMatchList; + final UnmodifiableRouteMatchList currentMatchList; /// Builder for route branch navigators (used for preloading). final BranchNavigatorPreloadBuilder branchNavigatorBuilder; @@ -122,9 +122,11 @@ class StatefulNavigationShellState extends State { return index; } - void _switchActiveBranch( - StatefulShellBranchState navigatorState, RouteMatchList? matchList) { + void _switchActiveBranch(StatefulShellBranchState navigatorState, + UnmodifiableRouteMatchList? unmodifiableRouteMatchList) { final GoRouter goRouter = GoRouter.of(context); + final RouteMatchList? matchList = + unmodifiableRouteMatchList?.routeMatchList; if (matchList != null && matchList.isNotEmpty) { goRouter.routeInformationParser .processRedirection(matchList, context) @@ -168,7 +170,8 @@ class StatefulNavigationShellState extends State { ); } return _copyStatefulShellBranchState(branchState, - navigatorBuilder: navigatorBuilder, matchList: matchList); + navigatorBuilder: navigatorBuilder, + matchList: matchList.unmodifiableRouteMatchList()); } return routeMatchList.then(createBranchNavigator); @@ -256,7 +259,7 @@ class StatefulNavigationShellState extends State { StatefulShellBranchState _copyStatefulShellBranchState( StatefulShellBranchState branchState, {BranchNavigatorBuilder? navigatorBuilder, - RouteMatchList? matchList, + UnmodifiableRouteMatchList? matchList, bool? loaded}) { if (navigatorBuilder != null) { final Navigator? existingNav = _navigatorForBranch(branchState.branch); @@ -270,14 +273,14 @@ class StatefulNavigationShellState extends State { loaded ?? _navigatorForBranch(branchState.branch) != null; return branchState.copy( child: branchWidget.copy(loaded: isLoaded), - matchList: matchList?.clone(), + matchList: matchList, ); } StatefulShellBranchState _createStatefulShellBranchState( StatefulShellBranch branch, {BranchNavigatorBuilder? navigatorBuilder, - RouteMatchList? matchList}) { + UnmodifiableRouteMatchList? matchList}) { if (navigatorBuilder != null) { _setNavigatorForBranch(branch, navigatorBuilder()); } @@ -287,7 +290,7 @@ class StatefulNavigationShellState extends State { branch: branch, navigatorForBranch: _navigatorForBranch, ), - matchList: matchList?.clone(), + matchList: matchList, ); } diff --git a/packages/go_router/lib/src/router.dart b/packages/go_router/lib/src/router.dart index 82a544e724ea..548747e5cec2 100644 --- a/packages/go_router/lib/src/router.dart +++ b/packages/go_router/lib/src/router.dart @@ -87,7 +87,6 @@ class GoRouter extends ChangeNotifier implements RouterConfig { refreshListenable: refreshListenable); _routerDelegate = GoRouterDelegate( - routeInformationProvider: _routeInformationProvider, configuration: _routeConfiguration, errorPageBuilder: errorPageBuilder, errorBuilder: errorBuilder, diff --git a/packages/go_router/lib/src/state.dart b/packages/go_router/lib/src/state.dart index 44910b27963d..efce962c5809 100644 --- a/packages/go_router/lib/src/state.dart +++ b/packages/go_router/lib/src/state.dart @@ -269,7 +269,8 @@ class StatefulShellRouteState { required this.route, required this.branchStates, required this.currentIndex, - required void Function(StatefulShellBranchState, RouteMatchList?) + required void Function( + StatefulShellBranchState, UnmodifiableRouteMatchList?) switchActiveBranch, required void Function() resetState, }) : _switchActiveBranch = switchActiveBranch, @@ -308,7 +309,7 @@ class StatefulShellRouteState { GlobalKey get currentNavigatorKey => currentBranchState.branch.navigatorKey; - final void Function(StatefulShellBranchState, RouteMatchList?) + final void Function(StatefulShellBranchState, UnmodifiableRouteMatchList?) _switchActiveBranch; final void Function() _resetState; @@ -404,12 +405,13 @@ class StatefulShellBranchState { const StatefulShellBranchState({ required this.branch, required this.child, - RouteMatchList? matchList, + UnmodifiableRouteMatchList? matchList, }) : _matchList = matchList; /// Constructs a copy of this [StatefulShellBranchState], with updated values for /// some of the fields. - StatefulShellBranchState copy({Widget? child, RouteMatchList? matchList}) { + StatefulShellBranchState copy( + {Widget? child, UnmodifiableRouteMatchList? matchList}) { return StatefulShellBranchState( branch: branch, child: child ?? this.child, @@ -430,7 +432,7 @@ class StatefulShellBranchState { final Widget child; /// The current navigation stack for the branch. - final RouteMatchList? _matchList; + final UnmodifiableRouteMatchList? _matchList; @override bool operator ==(Object other) { @@ -452,5 +454,5 @@ class StatefulShellBranchState { /// Helper extension on [StatefulShellBranchState], for internal use. extension StatefulShellBranchStateHelper on StatefulShellBranchState { /// The current navigation stack for the branch. - RouteMatchList? get matchList => _matchList; + UnmodifiableRouteMatchList? get matchList => _matchList; } diff --git a/packages/go_router/test/go_router_test.dart b/packages/go_router/test/go_router_test.dart index eafeb8d815e7..99921ecc1bb5 100644 --- a/packages/go_router/test/go_router_test.dart +++ b/packages/go_router/test/go_router_test.dart @@ -2935,10 +2935,14 @@ void main() { GlobalKey(); final GlobalKey sectionBNavigatorKey = GlobalKey(); + StatefulShellRouteState? routeState; final List routes = [ StatefulShellRoute( - builder: (_, __, Widget child) => child, + builder: (BuildContext context, GoRouterState state, Widget child) { + routeState = StatefulShellRoute.of(context); + return child; + }, routes: [ GoRoute( path: '/a', @@ -2996,6 +3000,16 @@ void main() { expect(find.text('Screen A Detail'), findsNothing); expect(find.text('Screen B'), findsOneWidget); expect(find.text('Screen B Detail'), findsNothing); + + routeState!.goBranch(index: 0); + await tester.pumpAndSettle(); + expect(find.text('Screen A'), findsNothing); + expect(find.text('Screen A Detail'), findsOneWidget); + + await simulateAndroidBackButton(tester); + await tester.pumpAndSettle(); + expect(find.text('Screen A'), findsOneWidget); + expect(find.text('Screen A Detail'), findsNothing); }); testWidgets( From 38b57723b710b54474a6504c77732cb4ddcf23f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20Lo=CC=88fstrand?= Date: Thu, 8 Dec 2022 16:31:52 +0100 Subject: [PATCH 062/112] Equality fix in UnmodifiableRouteMatchList. --- packages/go_router/lib/src/matching.dart | 2 +- packages/go_router/test/match_test.dart | 32 ++++++++++++++++++++++++ 2 files changed, 33 insertions(+), 1 deletion(-) diff --git a/packages/go_router/lib/src/matching.dart b/packages/go_router/lib/src/matching.dart index 5751e6cf3747..88e8a548e5ce 100644 --- a/packages/go_router/lib/src/matching.dart +++ b/packages/go_router/lib/src/matching.dart @@ -166,7 +166,7 @@ class UnmodifiableRouteMatchList { } return listEquals(other._matches, _matches) && other._uri == _uri && - other._pathParameters == _pathParameters; + mapEquals(other._pathParameters, _pathParameters); } @override diff --git a/packages/go_router/test/match_test.dart b/packages/go_router/test/match_test.dart index aa14db172468..cf9a10ac2b6e 100644 --- a/packages/go_router/test/match_test.dart +++ b/packages/go_router/test/match_test.dart @@ -6,6 +6,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:go_router/go_router.dart'; import 'package:go_router/src/match.dart'; +import 'package:go_router/src/matching.dart'; void main() { group('RouteMatch', () { @@ -136,6 +137,37 @@ void main() { expect(match1!.pageKey, match2!.pageKey); }); }); + + group('RouteMatchList', () { + test( + 'UnmodifiableRouteMatchList lists based on the same RouteMatchList are ' + 'equal', () { + final GoRoute route = GoRoute( + path: '/a', + builder: _builder, + ); + final RouteMatch match1 = RouteMatch( + route: route, + subloc: '/a', + extra: null, + error: null, + pageKey: const ValueKey('/a'), + ); + + final RouteMatchList list = RouteMatchList( + [match1], + Uri.parse('/'), + const {}, + ); + + final UnmodifiableRouteMatchList list1 = + list.unmodifiableRouteMatchList(); + final UnmodifiableRouteMatchList list2 = + list.unmodifiableRouteMatchList(); + + expect(list1, equals(list2)); + }); + }); } @immutable From d8d164142ae2adfb2c08a7aff785a16a90e39769 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20Lo=CC=88fstrand?= Date: Tue, 13 Dec 2022 21:42:31 +0100 Subject: [PATCH 063/112] Corrected invalid sample file names. --- packages/go_router/example/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/go_router/example/README.md b/packages/go_router/example/README.md index 2f555a5b48e0..1b5de5691383 100644 --- a/packages/go_router/example/README.md +++ b/packages/go_router/example/README.md @@ -31,13 +31,13 @@ An example to demonstrate how to use redirect to handle a synchronous sign-in fl An example to demonstrate how to use handle a sign-in flow with a stream authentication service. ## [Stateful Nested Navigation](https://github.com/flutter/packages/blob/main/packages/go_router/example/lib/stateful_shell_route.dart) -`flutter run lib/stateful_nested_navigation.dart` +`flutter run lib/stateful_shell_route.dart` An example to demonstrate how to use a `StatefulShellRoute` to create stateful nested navigation, with a `BottomNavigationBar`. ## [Dynamic Stateful Nested Navigation](https://github.com/flutter/packages/blob/main/packages/go_router/example/lib/stateful_shell_route_dynamic.dart) -`flutter run lib/dynamic_stateful_shell_branches.dart` +`flutter run lib/stateful_shell_route_dynamic.dart` An example to demonstrate how to use a `StatefulShellRoute` to create stateful nested navigation, with a `BottomNavigationBar` and a dynamic set of tabs and Navigators. From 4c4b7b0aa4cba10fe027efd4ba6441911d573800 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20Lo=CC=88fstrand?= Date: Tue, 13 Dec 2022 21:48:11 +0100 Subject: [PATCH 064/112] Additional rebuild improvement. Added isLoaded getter to StatefulShellBranchState. --- .../src/misc/stateful_navigation_shell.dart | 19 ++++++++++++++----- packages/go_router/lib/src/state.dart | 4 ++++ 2 files changed, 18 insertions(+), 5 deletions(-) diff --git a/packages/go_router/lib/src/misc/stateful_navigation_shell.dart b/packages/go_router/lib/src/misc/stateful_navigation_shell.dart index 6c348775ae17..79b30bd8cdaa 100644 --- a/packages/go_router/lib/src/misc/stateful_navigation_shell.dart +++ b/packages/go_router/lib/src/misc/stateful_navigation_shell.dart @@ -261,20 +261,29 @@ class StatefulNavigationShellState extends State { {BranchNavigatorBuilder? navigatorBuilder, UnmodifiableRouteMatchList? matchList, bool? loaded}) { + bool dirty = branchState.matchList != matchList; if (navigatorBuilder != null) { final Navigator? existingNav = _navigatorForBranch(branchState.branch); - if (existingNav == null || branchState.matchList != matchList) { + if (existingNav == null || dirty) { + dirty = true; _setNavigatorForBranch(branchState.branch, navigatorBuilder()); } } + final _BranchNavigatorProxy branchWidget = branchState.child as _BranchNavigatorProxy; final bool isLoaded = loaded ?? _navigatorForBranch(branchState.branch) != null; - return branchState.copy( - child: branchWidget.copy(loaded: isLoaded), - matchList: matchList, - ); + dirty = dirty || isLoaded != branchWidget.loaded; + + if (dirty) { + return branchState.copy( + child: branchWidget.copy(loaded: isLoaded), + matchList: matchList, + ); + } else { + return branchState; + } } StatefulShellBranchState _createStatefulShellBranchState( diff --git a/packages/go_router/lib/src/state.dart b/packages/go_router/lib/src/state.dart index efce962c5809..163a159568b7 100644 --- a/packages/go_router/lib/src/state.dart +++ b/packages/go_router/lib/src/state.dart @@ -434,6 +434,10 @@ class StatefulShellBranchState { /// The current navigation stack for the branch. final UnmodifiableRouteMatchList? _matchList; + /// Returns true if this branch has been loaded (i.e. visited once or + /// pre-loaded). + bool get isLoaded => _matchList != null; + @override bool operator ==(Object other) { if (identical(other, this)) { From 6f1b047ad40df4151f390f5b5839acdb59c4bae0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20Lo=CC=88fstrand?= Date: Wed, 14 Dec 2022 17:23:22 +0100 Subject: [PATCH 065/112] Minor documentation fix. --- packages/go_router/lib/src/route.dart | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/go_router/lib/src/route.dart b/packages/go_router/lib/src/route.dart index 4f6f27cabd93..815615fd076d 100644 --- a/packages/go_router/lib/src/route.dart +++ b/packages/go_router/lib/src/route.dart @@ -681,8 +681,6 @@ class StatefulShellRoute extends ShellRouteBase { /// This builder is used to provide the currently active StatefulShellBranches /// at any point in time. Each branch uses a separate [Navigator], identified /// by [StatefulShellBranch.navigatorKey]. - /// identifies the [Navigator] to be used - /// (via the navigatorKey) final StatefulShellBranchBuilder branchBuilder; /// Gets the state for the nearest stateful shell route in the Widget tree. From 9ffff5ca14ddb06ba3e6ea2d21d442044d489911 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20Lo=CC=88fstrand?= Date: Thu, 15 Dec 2022 10:20:53 +0100 Subject: [PATCH 066/112] Removed AppRouterProvider from sample code. --- .../example/lib/stateful_shell_route.dart | 53 ++++--------------- 1 file changed, 9 insertions(+), 44 deletions(-) diff --git a/packages/go_router/example/lib/stateful_shell_route.dart b/packages/go_router/example/lib/stateful_shell_route.dart index b0dd6d6d2a7c..aa49cba5e7e4 100644 --- a/packages/go_router/example/lib/stateful_shell_route.dart +++ b/packages/go_router/example/lib/stateful_shell_route.dart @@ -35,37 +35,6 @@ void main() { runApp(NestedTabNavigationExampleApp()); } -/// InheritedWidget that provides a reference to the GoRouter in the widget -/// tree, without creating a dependency that triggers rebuilds. -/// -/// Simply use AppRouterProvider.of as an alternative to GoRouter.of, to get a -/// reference to the GoRouter, that doesn't cause rebuilds every time GoRouter -/// (or rather it's current location) changes. -class AppRouterProvider extends InheritedWidget { - /// Constructs an [AppRouterProvider]. - const AppRouterProvider({ - required Widget child, - required this.goRouter, - Key? key, - }) : super(child: child, key: key); - - /// The [GoRouter] instance used for this application. - final GoRouter goRouter; - - @override - bool updateShouldNotify(covariant AppRouterProvider oldWidget) { - return false; - } - - /// Find the current GoRouter in the widget tree. - static GoRouter of(BuildContext context) { - final AppRouterProvider? inherited = - context.dependOnInheritedWidgetOfExactType(); - assert(inherited != null, 'No GoRouter found in context'); - return inherited!.goRouter; - } -} - /// An example demonstrating how to use nested navigators class NestedTabNavigationExampleApp extends StatelessWidget { /// Creates a NestedTabNavigationExampleApp @@ -192,15 +161,12 @@ class NestedTabNavigationExampleApp extends StatelessWidget { @override Widget build(BuildContext context) { - return AppRouterProvider( - goRouter: _router, - child: MaterialApp.router( - title: 'Flutter Demo', - theme: ThemeData( - primarySwatch: Colors.blue, - ), - routerConfig: _router, + return MaterialApp.router( + title: 'Flutter Demo', + theme: ThemeData( + primarySwatch: Colors.blue, ), + routerConfig: _router, ); } } @@ -271,8 +237,7 @@ class RootScreen extends StatelessWidget { const Padding(padding: EdgeInsets.all(4)), TextButton( onPressed: () { - AppRouterProvider.of(context) - .go(detailsPath, extra: '$label-XYZ'); + GoRouter.of(context).go(detailsPath, extra: '$label-XYZ'); }, child: const Text('View details'), ), @@ -280,7 +245,7 @@ class RootScreen extends StatelessWidget { if (secondDetailsPath != null) TextButton( onPressed: () { - AppRouterProvider.of(context).go(secondDetailsPath!); + GoRouter.of(context).go(secondDetailsPath!); }, child: const Text('View more details'), ), @@ -367,7 +332,7 @@ class DetailsScreenState extends State { const Padding(padding: EdgeInsets.all(16)), TextButton( onPressed: () { - AppRouterProvider.of(context).pop(); + GoRouter.of(context).pop(); }, child: const Text('< Back', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 18)), @@ -444,7 +409,7 @@ class TabScreen extends StatelessWidget { if (detailsPath != null) TextButton( onPressed: () { - AppRouterProvider.of(context).go(detailsPath!); + GoRouter.of(context).go(detailsPath!); }, child: const Text('View details'), ), From 0ea48cbaecfb3e502f2fb530a28d732344344948 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20L=C3=B6fstrand?= Date: Wed, 21 Dec 2022 09:43:18 +0100 Subject: [PATCH 067/112] Update packages/go_router/CHANGELOG.md Wording fix Co-authored-by: chunhtai <47866232+chunhtai@users.noreply.github.com> --- packages/go_router/CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/go_router/CHANGELOG.md b/packages/go_router/CHANGELOG.md index 85ed071d7210..475da8f1dbf1 100644 --- a/packages/go_router/CHANGELOG.md +++ b/packages/go_router/CHANGELOG.md @@ -1,6 +1,6 @@ ## 5.3.0 -- Introduced a new shell route class called `StatefulShellRoute`, to support using separate +- Introduces `StatefulShellRoute` to support using separate navigators for child routes as well as preserving state in each navigation tree (flutter/flutter#99124). - Updated documentation for `pageBuilder` and `builder` fields of `ShellRoute`, to more correctly From 4abcaa9057518e1c36d40db303a8ca8d837746a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20L=C3=B6fstrand?= Date: Wed, 21 Dec 2022 09:43:31 +0100 Subject: [PATCH 068/112] Update packages/go_router/CHANGELOG.md Wording fix Co-authored-by: chunhtai <47866232+chunhtai@users.noreply.github.com> --- packages/go_router/CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/go_router/CHANGELOG.md b/packages/go_router/CHANGELOG.md index 475da8f1dbf1..3fef39f5f777 100644 --- a/packages/go_router/CHANGELOG.md +++ b/packages/go_router/CHANGELOG.md @@ -3,7 +3,7 @@ - Introduces `StatefulShellRoute` to support using separate navigators for child routes as well as preserving state in each navigation tree (flutter/flutter#99124). -- Updated documentation for `pageBuilder` and `builder` fields of `ShellRoute`, to more correctly +- Updates documentation for `pageBuilder` and `builder` fields of `ShellRoute`, to more correctly describe the meaning of the child argument in the builder functions. - Added support for restorationId to ShellRoute (and StatefulShellRoute). From ce23558cb213ea514adc4b30320a0447c6a36d65 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20L=C3=B6fstrand?= Date: Wed, 21 Dec 2022 09:43:42 +0100 Subject: [PATCH 069/112] Update packages/go_router/CHANGELOG.md Wording fix Co-authored-by: chunhtai <47866232+chunhtai@users.noreply.github.com> --- packages/go_router/CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/go_router/CHANGELOG.md b/packages/go_router/CHANGELOG.md index 3fef39f5f777..cd24b55c4a82 100644 --- a/packages/go_router/CHANGELOG.md +++ b/packages/go_router/CHANGELOG.md @@ -5,7 +5,7 @@ (flutter/flutter#99124). - Updates documentation for `pageBuilder` and `builder` fields of `ShellRoute`, to more correctly describe the meaning of the child argument in the builder functions. -- Added support for restorationId to ShellRoute (and StatefulShellRoute). +- Adds support for restorationId to ShellRoute (and StatefulShellRoute). ## 5.2.4 From 351ceb20402c3a5538a466e3169b11145a8bea9a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20L=C3=B6fstrand?= Date: Wed, 21 Dec 2022 09:50:35 +0100 Subject: [PATCH 070/112] Update packages/go_router/lib/src/route.dart Documentation wording fix Co-authored-by: chunhtai <47866232+chunhtai@users.noreply.github.com> --- packages/go_router/lib/src/route.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/go_router/lib/src/route.dart b/packages/go_router/lib/src/route.dart index 815615fd076d..e3b8a2dbc4db 100644 --- a/packages/go_router/lib/src/route.dart +++ b/packages/go_router/lib/src/route.dart @@ -323,7 +323,7 @@ class GoRoute extends RouteBase { late final RegExp _pathRE; } -/// Base class for classes that acts as a shell for sub-routes, such +/// Base class for classes that act as shells for sub-routes, such /// as [ShellRoute] and [StatefulShellRoute]. abstract class ShellRouteBase extends RouteBase { const ShellRouteBase._({this.builder, this.pageBuilder, super.routes}) From 4cb0f1e6b2f102665d514fc076e970a8b2316633 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20Lo=CC=88fstrand?= Date: Thu, 5 Jan 2023 01:13:10 +0100 Subject: [PATCH 071/112] Partially reverted the StatefulShellRoute API back to the previous solution with static branches. --- packages/go_router/example/README.md | 6 - .../example/lib/stateful_shell_route.dart | 207 ++--- .../lib/stateful_shell_route_dynamic.dart | 283 ------- packages/go_router/lib/src/builder.dart | 93 +-- packages/go_router/lib/src/configuration.dart | 195 +++-- packages/go_router/lib/src/delegate.dart | 13 +- .../src/misc/stateful_navigation_shell.dart | 207 +++-- packages/go_router/lib/src/route.dart | 328 ++++---- packages/go_router/lib/src/shell_state.dart | 233 ++++++ packages/go_router/lib/src/state.dart | 208 ----- packages/go_router/test/builder_test.dart | 336 ++++---- .../go_router/test/configuration_test.dart | 399 ++++++++-- packages/go_router/test/delegate_test.dart | 15 +- packages/go_router/test/go_router_test.dart | 718 ++++++++---------- 14 files changed, 1555 insertions(+), 1686 deletions(-) delete mode 100644 packages/go_router/example/lib/stateful_shell_route_dynamic.dart create mode 100644 packages/go_router/lib/src/shell_state.dart diff --git a/packages/go_router/example/README.md b/packages/go_router/example/README.md index 1b5de5691383..5ee817ac7b41 100644 --- a/packages/go_router/example/README.md +++ b/packages/go_router/example/README.md @@ -36,12 +36,6 @@ An example to demonstrate how to use handle a sign-in flow with a stream authent An example to demonstrate how to use a `StatefulShellRoute` to create stateful nested navigation, with a `BottomNavigationBar`. -## [Dynamic Stateful Nested Navigation](https://github.com/flutter/packages/blob/main/packages/go_router/example/lib/stateful_shell_route_dynamic.dart) -`flutter run lib/stateful_shell_route_dynamic.dart` - -An example to demonstrate how to use a `StatefulShellRoute` to create stateful nested navigation, with a -`BottomNavigationBar` and a dynamic set of tabs and Navigators. - ## [Books app](https://github.com/flutter/packages/blob/main/packages/go_router/example/lib/books) `flutter run lib/books/main.dart` diff --git a/packages/go_router/example/lib/stateful_shell_route.dart b/packages/go_router/example/lib/stateful_shell_route.dart index aa49cba5e7e4..e862ec5f5b98 100644 --- a/packages/go_router/example/lib/stateful_shell_route.dart +++ b/packages/go_router/example/lib/stateful_shell_route.dart @@ -6,20 +6,8 @@ import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; -final List _bottomNavBranches = [ - StatefulShellBranch(rootLocation: '/a', name: 'A'), - StatefulShellBranch(rootLocation: '/b', name: 'B'), - StatefulShellBranch(rootLocations: const ['/c1', '/c2'], name: 'C'), - - /// To enable preloading of the root routes of the branches, pass true - /// for the parameter preload of StatefulShellBranch. - //StatefulShellBranch(rootLocations: ['/c1', '/c2'], name: 'C', preload: true), -]; - -final List _topNavBranches = [ - StatefulShellBranch(rootLocation: '/c1', name: 'C1'), - StatefulShellBranch(rootLocation: '/c2', name: 'C2'), -]; +final GlobalKey _tabANavigatorKey = + GlobalKey(debugLabel: 'tabANav'); // This example demonstrates how to setup nested navigation using a // BottomNavigationBar, where each tab uses its own persistent navigator, i.e. @@ -44,94 +32,132 @@ class NestedTabNavigationExampleApp extends StatelessWidget { initialLocation: '/a', routes: [ StatefulShellRoute( - routes: [ - GoRoute( - /// The screen to display as the root in the first tab of the - /// bottom navigation bar. - path: '/a', - builder: (BuildContext context, GoRouterState state) => - const RootScreen(label: 'A', detailsPath: '/a/details'), + /// To enable preloading of the root routes of the branches, pass true + /// for the parameter preloadBranches. + // preloadBranches: true, + branches: [ + /// The route branch for the first tab of the bottom navigation bar. + StatefulShellBranch( + navigatorKey: _tabANavigatorKey, routes: [ - /// The details screen to display stacked on navigator of the - /// first tab. This will cover screen A but not the application - /// shell (bottom navigation bar). GoRoute( - path: 'details', + /// The screen to display as the root in the first tab of the + /// bottom navigation bar. + path: '/a', builder: (BuildContext context, GoRouterState state) => - DetailsScreen(label: 'A', extra: state.extra), + const RootScreen(label: 'A', detailsPath: '/a/details'), + routes: [ + /// The details screen to display stacked on navigator of the + /// first tab. This will cover screen A but not the application + /// shell (bottom navigation bar). + GoRoute( + path: 'details', + builder: (BuildContext context, GoRouterState state) => + DetailsScreen(label: 'A', extra: state.extra), + ), + ], ), ], ), - GoRoute( - /// The screen to display as the root in the second tab of the - /// bottom navigation bar. - path: '/b', - builder: (BuildContext context, GoRouterState state) => - const RootScreen( - label: 'B', - detailsPath: '/b/details/1', - secondDetailsPath: '/b/details/2', - ), + + /// The route branch for the second tab of the bottom navigation bar. + StatefulShellBranch( + /// It's not necessary to provide a navigatorKey if it isn't also + /// needed elsewhere. If not provided, a default key will be used. + // navigatorKey: _tabBNavigatorKey, routes: [ GoRoute( - path: 'details/:param', + /// The screen to display as the root in the second tab of the + /// bottom navigation bar. + path: '/b', builder: (BuildContext context, GoRouterState state) => - DetailsScreen( + const RootScreen( label: 'B', - param: state.params['param'], - extra: state.extra, + detailsPath: '/b/details/1', + secondDetailsPath: '/b/details/2', ), + routes: [ + GoRoute( + path: 'details/:param', + builder: (BuildContext context, GoRouterState state) => + DetailsScreen( + label: 'B', + param: state.params['param'], + extra: state.extra, + ), + ), + ], ), ], ), - StatefulShellRoute( - routes: [ - GoRoute( - path: '/c1', - builder: (BuildContext context, GoRouterState state) => - const TabScreen(label: 'C1', detailsPath: '/c1/details'), - routes: [ + + /// The route branch for the third tab of the bottom navigation bar. + StatefulShellBranch( + /// ShellRouteBranch will automatically use the first descendant + /// GoRoute as the default location of the branch. If another route + /// is desired, you can specify the location of it using the + /// defaultLocation parameter. + // defaultLocation: '/c2', + routes: [ + StatefulShellRoute( + /// This bottom tab uses a nested shell, wrapping sub routes in a + /// top TabBar. + branches: [ + StatefulShellBranch(routes: [ GoRoute( - path: 'details', + path: '/c1', builder: (BuildContext context, GoRouterState state) => - DetailsScreen( - label: 'C1', - extra: state.extra, - withScaffold: false, - ), + const TabScreen( + label: 'C1', detailsPath: '/c1/details'), + routes: [ + GoRoute( + path: 'details', + builder: + (BuildContext context, GoRouterState state) => + DetailsScreen( + label: 'C1', + extra: state.extra, + withScaffold: false, + ), + ), + ], ), - ], - ), - GoRoute( - path: '/c2', - builder: (BuildContext context, GoRouterState state) => - const TabScreen(label: 'C2', detailsPath: '/c2/details'), - routes: [ + ]), + StatefulShellBranch(routes: [ GoRoute( - path: 'details', + path: '/c2', builder: (BuildContext context, GoRouterState state) => - DetailsScreen( - label: 'C2', - extra: state.extra, - withScaffold: false, - ), + const TabScreen( + label: 'C2', detailsPath: '/c2/details'), + routes: [ + GoRoute( + path: 'details', + builder: + (BuildContext context, GoRouterState state) => + DetailsScreen( + label: 'C2', + extra: state.extra, + withScaffold: false, + ), + ), + ], ), - ], - ), - ], - branches: _topNavBranches, - builder: - (BuildContext context, GoRouterState state, Widget child) { - /// For this nested StatefulShellRoute we are using a custom - /// container (TabBarView) for the branch navigators, and thus - /// ignoring the default navigator contained passed to the - /// builder. Custom implementation can access the branch - /// navigators via the StatefulShellRouteState - /// (see TabbedRootScreen for details). - return const TabbedRootScreen(); - }), + ]), + ], + builder: + (BuildContext context, GoRouterState state, Widget child) { + /// For this nested StatefulShellRoute we are using a custom + /// container (TabBarView) for the branch navigators, and thus + /// ignoring the default navigator contained passed to the + /// builder. Custom implementation can access the branch + /// navigators via the StatefulShellRouteState + /// (see TabbedRootScreen for details). + return const TabbedRootScreen(); + }, + ), + ], + ), ], - branches: _bottomNavBranches, builder: (BuildContext context, GoRouterState state, Widget child) { return ScaffoldWithNavBar(body: child); }, @@ -140,9 +166,8 @@ class NestedTabNavigationExampleApp extends StatelessWidget { /// for instance setup custom animations, you can implement your builder /// something like below (see _AnimatedRouteBranchContainer). Note that /// in this case, you should not add the Widget provided in the child - /// parameter of the builder to the widget tree. Instead, you should use - /// the child widgets of each branch - /// (see StatefulShellRouteState.children). + /// parameter to the widget tree. Instead, you should use the child + /// widgets of each branch (see StatefulShellRouteState.children). // builder: (BuildContext context, GoRouterState state, Widget child) { // return ScaffoldWithNavBar( // body: _AnimatedRouteBranchContainer(), @@ -185,7 +210,8 @@ class ScaffoldWithNavBar extends StatelessWidget { @override Widget build(BuildContext context) { - final StatefulShellRouteState shellState = StatefulShellRoute.of(context); + final StatefulShellRouteState shellState = + StatefulShellRouteState.of(context); return Scaffold( body: body, @@ -196,8 +222,7 @@ class ScaffoldWithNavBar extends StatelessWidget { BottomNavigationBarItem(icon: Icon(Icons.tab), label: 'Section C'), ], currentIndex: shellState.currentIndex, - onTap: (int tappedIndex) => shellState.goBranch( - navigatorKey: _bottomNavBranches[tappedIndex].navigatorKey), + onTap: (int tappedIndex) => shellState.goBranch(index: tappedIndex), ), ); } @@ -353,7 +378,8 @@ class TabbedRootScreen extends StatelessWidget { @override Widget build(BuildContext context) { - final StatefulShellRouteState shellState = StatefulShellRoute.of(context); + final StatefulShellRouteState shellState = + StatefulShellRouteState.of(context); final List children = shellState.branchStates.map(_child).toList(); final List tabs = children.mapIndexed((int i, _) => Tab(text: 'Tab ${i + 1}')).toList(); @@ -376,8 +402,7 @@ class TabbedRootScreen extends StatelessWidget { } void _onTabTap(BuildContext context, int index) { - StatefulShellRoute.of(context) - .goBranch(navigatorKey: _topNavBranches[index].navigatorKey); + StatefulShellRouteState.of(context).goBranch(index: index); } } @@ -424,7 +449,7 @@ class _AnimatedRouteBranchContainer extends StatelessWidget { @override Widget build(BuildContext context) { final StatefulShellRouteState shellRouteState = - StatefulShellRoute.of(context); + StatefulShellRouteState.of(context); final int currentIndex = shellRouteState.currentIndex; return Stack( children: shellRouteState.children.mapIndexed( diff --git a/packages/go_router/example/lib/stateful_shell_route_dynamic.dart b/packages/go_router/example/lib/stateful_shell_route_dynamic.dart deleted file mode 100644 index 53482d6dd8b8..000000000000 --- a/packages/go_router/example/lib/stateful_shell_route_dynamic.dart +++ /dev/null @@ -1,283 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'package:collection/collection.dart'; -import 'package:flutter/material.dart'; -import 'package:go_router/go_router.dart'; - -final GlobalKey _rootNavigatorKey = - GlobalKey(debugLabel: 'root'); - -// This example demonstrates how to setup nested navigation using a -// BottomNavigationBar, using a dynamic set of tabs. Each tab uses its own -// persistent navigator, i.e. navigation state is maintained separately for each -// tab. This setup also enables deep linking into nested pages. -// -// This example demonstrates how to display routes within a StatefulShellRoute, -// that are places on separate navigators. The example also demonstrates how -// state is maintained when switching between different tabs (and thus branches -// and Navigators), as well as how to maintain a dynamic set of branches/tabs. - -void main() { - runApp(const NestedTabNavigationExampleApp()); -} - -/// An example demonstrating how to use nested navigators -class NestedTabNavigationExampleApp extends StatefulWidget { - /// Creates a NestedTabNavigationExampleApp - const NestedTabNavigationExampleApp({Key? key}) : super(key: key); - - @override - State createState() => NestedTabNavigationExampleAppState(); -} - -/// An example demonstrating how to use dynamic nested navigators -class NestedTabNavigationExampleAppState - extends State { - final List _branches = [ - StatefulShellBranch(rootLocation: '/home', name: 'Home'), - StatefulShellBranch(rootLocation: '/a/0', name: 'Dynamic 0'), - ]; - - void _addSection(StatefulShellRouteState shellRouteState) => setState(() { - if (_branches.length < 10) { - final int index = _branches.length - 1; - _branches.add(StatefulShellBranch( - rootLocation: '/a/$index', name: 'Dynamic $index')); - // In situations where setState isn't possible, you can call refresh() on - // StatefulShellRouteState instead, to refresh the branches - //shellRouteState.refresh(); - } - }); - - void _removeSection(StatefulShellRouteState shellRouteState) { - if (_branches.length > 2) { - _branches.removeLast(); - // In situations where setState isn't possible, you can call refresh() on - // StatefulShellRouteState instead, to refresh the branches - shellRouteState.refresh(); - } - } - - late final GoRouter _router = GoRouter( - navigatorKey: _rootNavigatorKey, - initialLocation: '/home', - routes: [ - StatefulShellRoute( - branchBuilder: (_, __) => _branches, - routes: [ - GoRoute( - path: '/home', - builder: (BuildContext context, GoRouterState state) => RootScreen( - label: 'Home', - addSection: () => _addSection(StatefulShellRoute.of(context)), - removeSection: () => - _removeSection(StatefulShellRoute.of(context)), - ), - ), - GoRoute( - /// The screen to display as the root in the first tab of the - /// bottom navigation bar. - path: '/a/:id', - builder: (BuildContext context, GoRouterState state) => RootScreen( - label: 'A${state.params['id']}', - detailsPath: '/a/${state.params['id']}/details', - ), - routes: [ - /// The details screen to display stacked on navigator of the - /// first tab. This will cover screen A but not the application - /// shell (bottom navigation bar). - GoRoute( - path: 'details', - builder: (BuildContext context, GoRouterState state) => - DetailsScreen(label: 'A${state.params['id']}'), - ), - ], - ), - ], - builder: (BuildContext context, GoRouterState state, Widget child) { - return ScaffoldWithNavBar(body: child); - }, - ), - ], - ); - - @override - Widget build(BuildContext context) { - return MaterialApp.router( - title: 'Flutter Demo', - theme: ThemeData( - primarySwatch: Colors.blue, - ), - routerConfig: _router, - ); - } -} - -/// Builds the "shell" for the app by building a Scaffold with a -/// BottomNavigationBar, where [child] is placed in the body of the Scaffold. -class ScaffoldWithNavBar extends StatelessWidget { - /// Constructs an [ScaffoldWithNavBar]. - const ScaffoldWithNavBar({ - required this.body, - Key? key, - }) : super(key: key ?? const ValueKey('ScaffoldWithNavBar')); - - /// Body, i.e. the index stack - final Widget body; - - List _items( - List branches) { - return branches.mapIndexed((int i, StatefulShellBranchState e) { - if (i == 0) { - return BottomNavigationBarItem( - icon: const Icon(Icons.home), label: e.branch.name); - } else { - return BottomNavigationBarItem( - icon: const Icon(Icons.star), label: e.branch.name); - } - }).toList(); - } - - @override - Widget build(BuildContext context) { - final StatefulShellRouteState shellState = StatefulShellRoute.of(context); - return Scaffold( - body: body, - bottomNavigationBar: BottomNavigationBar( - type: BottomNavigationBarType.fixed, - items: _items(shellState.branchStates), - currentIndex: shellState.currentIndex, - onTap: (int tappedIndex) => shellState.goBranch(index: tappedIndex), - ), - ); - } -} - -/// Widget for the root/initial pages in the bottom navigation bar. -class RootScreen extends StatelessWidget { - /// Creates a RootScreen - const RootScreen({ - required this.label, - this.detailsPath, - this.addSection, - this.removeSection, - Key? key, - }) : super(key: key); - - /// The label - final String label; - - /// The path to the detail page - final String? detailsPath; - - /// Function for adding a new branch - final VoidCallback? addSection; - - /// Function for removing a branch - final VoidCallback? removeSection; - - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar( - title: Text('Section - $label'), - ), - body: Center( - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Text('Screen $label', - style: Theme.of(context).textTheme.titleLarge), - const Padding(padding: EdgeInsets.all(4)), - if (detailsPath != null) - TextButton( - onPressed: () { - GoRouter.of(context).go(detailsPath!); - }, - child: const Text('View details'), - ), - if (addSection != null && removeSection != null) - ..._actions(context), - ], - ), - ), - ); - } - - List _actions(BuildContext context) { - return [ - const Padding(padding: EdgeInsets.all(4)), - TextButton( - onPressed: addSection, - child: const Text('Add section'), - ), - const Padding(padding: EdgeInsets.all(4)), - TextButton( - onPressed: removeSection, - child: const Text('Remove section'), - ), - ]; - } -} - -/// The details screen for either the A or B screen. -class DetailsScreen extends StatefulWidget { - /// Constructs a [DetailsScreen]. - const DetailsScreen({ - required this.label, - this.param, - Key? key, - }) : super(key: key); - - /// The label to display in the center of the screen. - final String label; - - /// Optional param - final String? param; - - @override - State createState() => DetailsScreenState(); -} - -/// The state for DetailsScreen -class DetailsScreenState extends State { - int _counter = 0; - - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar( - title: Text('Details Screen - ${widget.label}'), - ), - body: _build(context), - ); - } - - Widget _build(BuildContext context) { - return Center( - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Text('Details for ${widget.label} - Counter: $_counter', - style: Theme.of(context).textTheme.titleLarge), - const Padding(padding: EdgeInsets.all(4)), - TextButton( - onPressed: () { - setState(() { - _counter++; - }); - }, - child: const Text('Increment counter'), - ), - const Padding(padding: EdgeInsets.all(8)), - if (widget.param != null) - Text('Parameter: ${widget.param!}', - style: Theme.of(context).textTheme.titleMedium), - const Padding(padding: EdgeInsets.all(8)), - ], - ), - ); - } -} diff --git a/packages/go_router/lib/src/builder.dart b/packages/go_router/lib/src/builder.dart index 3cb707d1e840..36181a3492c4 100644 --- a/packages/go_router/lib/src/builder.dart +++ b/packages/go_router/lib/src/builder.dart @@ -181,55 +181,35 @@ class RouteBuilder { // that the page for this ShellRoute is placed at the right index. final int shellPageIdx = keyToPages[parentNavigatorKey]!.length; - void buildRecursive(GlobalKey shellNavigatorKey) { - // Add an entry for the shell route's navigator - keyToPages.putIfAbsent(shellNavigatorKey, () => >[]); + // Get the current sub-route of this shell route from the match list. + final RouteBase subRoute = matchList.matches[startIndex + 1].route; - // Build the remaining pages - _buildRecursive(context, matchList, startIndex + 1, onPopPage, - routerNeglect, keyToPages, shellNavigatorKey, registry); - } + // The key to provide to the shell route's Navigator. + final GlobalKey shellNavigatorKey = + route.navigatorKeyForSubRoute(subRoute); + + // Add an entry for the shell route's navigator + keyToPages.putIfAbsent(shellNavigatorKey, () => >[]); + + // Build the remaining pages + _buildRecursive(context, matchList, startIndex + 1, onPopPage, + routerNeglect, keyToPages, shellNavigatorKey, registry); // Build the Navigator and/or StatefulNavigationShell - Widget? child; + Navigator buildNavigator(String? restorationScopeId) => _buildNavigator( + onPopPage, keyToPages[shellNavigatorKey]!, shellNavigatorKey, + restorationScopeId: restorationScopeId); + Widget child; if (route is StatefulShellRoute) { - final List branches = - route.branchBuilder(context, state); - final StatefulShellBranch? branch = - route.resolveBranch(branches, state); - // Since branches may be generated dynamically, validation needs to - // occur at build time, rather than at creation of RouteConfiguration - assert(() { - return configuration.debugValidateStatefulShellBranches( - route, branches); - }()); - - if (branch == null) { - throw _RouteBuilderError('Shell routes must always provide a ' - 'navigator key for its immediate sub-routes'); - } - // The key to provide to the shell route's Navigator. - final GlobalKey shellNavigatorKey = branch.navigatorKey; - buildRecursive(shellNavigatorKey); - - Navigator buildBranchNavigator() { - return _buildNavigator( - onPopPage, keyToPages[shellNavigatorKey]!, shellNavigatorKey, - restorationScopeId: branch.restorationScopeId); - } - - child = _buildStatefulNavigationShell(route, state, branches, branch, - buildBranchNavigator, matchList, onPopPage, registry); - } else if (route is ShellRoute) { - // The key to provide to the shell route's Navigator. - final GlobalKey shellNavigatorKey = route.navigatorKey; - buildRecursive(shellNavigatorKey); - final String? restorationScopeId = route.restorationScopeId; - child = _buildNavigator( - onPopPage, keyToPages[shellNavigatorKey]!, shellNavigatorKey, - restorationScopeId: restorationScopeId); + final String? restorationScopeId = + route.branchForSubRoute(subRoute).restorationScopeId; + child = buildNavigator(restorationScopeId); + child = _buildStatefulNavigationShell( + route, child as Navigator, state, matchList, onPopPage, registry); } else { - assert(false, 'Unknown route type ($route)'); + final String? restorationScopeId = + (route is ShellRoute) ? route.restorationScopeId : null; + child = buildNavigator(restorationScopeId); } // Build the Page for this route @@ -261,10 +241,8 @@ class RouteBuilder { StatefulNavigationShell _buildStatefulNavigationShell( StatefulShellRoute shellRoute, + Navigator navigator, GoRouterState shellRouterState, - List branches, - StatefulShellBranch currentBranch, - BranchNavigatorBuilder currentNavigatorBuilder, RouteMatchList currentMatchList, PopPageCallback pop, Map, GoRouterState> registry, @@ -273,11 +251,9 @@ class RouteBuilder { configuration: configuration, shellRoute: shellRoute, shellGoRouterState: shellRouterState, - branches: branches, - currentBranch: currentBranch, - currentNavigatorBuilder: currentNavigatorBuilder, + currentNavigator: navigator, currentMatchList: currentMatchList.unmodifiableRouteMatchList(), - branchNavigatorBuilder: (BuildContext context, + branchPreloadNavigatorBuilder: (BuildContext context, RouteMatchList matchList, int startIndex, GlobalKey navigatorKey, @@ -289,11 +265,6 @@ class RouteBuilder { }); } - /// Gets the current [StatefulShellBranch] for the provided - /// [StatefulShellRoute]. - StatefulShellBranch? currentStatefulShellBranch(StatefulShellRoute route) => - route.currentBranch; - /// Helper method that builds a [GoRouterState] object for the given [match] /// and [params]. @visibleForTesting @@ -336,15 +307,9 @@ class RouteBuilder { if (pageBuilder != null) { page = pageBuilder(context, state); } - } else if (route is StatefulShellRoute) { - final ShellRoutePageBuilder? pageForShell = route.pageBuilder; - assert(child != null, 'StatefulShellRoute must contain a child route'); - if (pageForShell != null) { - page = pageForShell(context, state, child!); - } - } else if (route is ShellRoute) { + } else if (route is ShellRouteBase) { final ShellRoutePageBuilder? pageBuilder = route.pageBuilder; - assert(child != null, 'ShellRoute must contain a child route'); + assert(child != null, '${route.runtimeType} must contain a child route'); if (pageBuilder != null) { page = pageBuilder(context, state, child!); } diff --git a/packages/go_router/lib/src/configuration.dart b/packages/go_router/lib/src/configuration.dart index e013197741bb..bbf9be341156 100644 --- a/packages/go_router/lib/src/configuration.dart +++ b/packages/go_router/lib/src/configuration.dart @@ -12,6 +12,7 @@ import 'misc/errors.dart'; import 'path_utils.dart'; import 'typedefs.dart'; export 'route.dart'; +export 'shell_state.dart'; export 'state.dart'; /// The route configuration for GoRouter configured by the app. @@ -27,6 +28,8 @@ class RouteConfiguration { _debugVerifyNoDuplicatePathParameter(routes, {})), assert(_debugCheckParentNavigatorKeys( routes, >[navigatorKey])) { + assert(_debugCheckShellRouteBranchDefaultLocations( + routes, RouteMatcher(this))); _cacheNameToPath('', routes); log.info(_debugKnownRoutes()); } @@ -89,6 +92,12 @@ class RouteConfiguration { >[...allowedKeys..add(route.navigatorKey)], ); } else if (route is StatefulShellRoute) { + for (final StatefulShellBranch branch in route.branches) { + assert( + !allowedKeys.contains(branch.navigatorKey), + 'StatefulShellBranch must not reuse an ancestor navigatorKey ' + '(${branch.navigatorKey})'); + } _debugCheckParentNavigatorKeys(route.routes, allowedKeys); } } @@ -115,6 +124,90 @@ class RouteConfiguration { return true; } + // Check to see that the configured defaultLocation of StatefulShellBranches + // points to a descendant route of the route branch. + bool _debugCheckShellRouteBranchDefaultLocations( + List routes, RouteMatcher matcher) { + try { + for (final RouteBase route in routes) { + if (route is StatefulShellRoute) { + for (final StatefulShellBranch branch in route.branches) { + if (branch.defaultLocation == null) { + // Recursively search for the first GoRoute descendant. Will + // throw assertion error if not found. + findShellRouteBranchDefaultLocation(branch); + } else { + final RouteBase defaultLocationRoute = + matcher.findMatch(branch.defaultLocation!).last.route; + final RouteBase? match = branch.routes.firstWhereOrNull( + (RouteBase e) => _debugIsDescendantOrSame( + ancestor: e, route: defaultLocationRoute)); + assert( + match != null, + 'The defaultLocation (${branch.defaultLocation}) of ' + 'StatefulShellBranch must match a descendant route of the ' + 'branch'); + } + } + } + _debugCheckShellRouteBranchDefaultLocations(route.routes, matcher); + } + } on MatcherError catch (e) { + assert( + false, + 'defaultLocation (${e.location}) of StatefulShellBranch must ' + 'be a valid location'); + } + return true; + } + + static Iterable _subRoutesRecursively(List routes) => + routes.expand( + (RouteBase e) => [e, ..._subRoutesRecursively(e.routes)]); + + static GoRoute? _findFirstGoRoute(List routes) => + _subRoutesRecursively(routes) + .firstWhereOrNull((RouteBase e) => e is GoRoute) as GoRoute?; + + /// Tests if a route is a descendant of, or same as, an ancestor route. + bool _debugIsDescendantOrSame( + {required RouteBase ancestor, required RouteBase route}) => + ancestor == route || + _subRoutesRecursively(ancestor.routes).contains(route); + + /// Recursively traverses the routes of the provided ShellRouteBranch to find + /// the first GoRoute, from which a full path will be derived. + String findShellRouteBranchDefaultLocation(StatefulShellBranch branch) { + final GoRoute? route = _findFirstGoRoute(branch.routes); + final String? defaultLocation = + route != null ? _fullPathForRoute(route, '', routes) : null; + assert( + defaultLocation != null, + 'The default location of a ShellRouteBranch' + ' must be configured or derivable from GoRoute descendant'); + return defaultLocation!; + } + + static String? _fullPathForRoute( + RouteBase targetRoute, String parentFullpath, List routes) { + for (final RouteBase route in routes) { + final String fullPath = (route is GoRoute) + ? concatenatePaths(parentFullpath, route.path) + : parentFullpath; + + if (route == targetRoute) { + return fullPath; + } else { + final String? subRoutePath = + _fullPathForRoute(targetRoute, fullPath, route.routes); + if (subRoutePath != null) { + return subRoutePath; + } + } + } + return null; + } + /// The list of top level routes used by [GoRouterDelegate]. final List routes; @@ -129,9 +222,6 @@ class RouteConfiguration { final Map _nameToPath = {}; - late final Set> _debugAllNavigatorKeys = - _debugAllNavigatorsRecursively(routes); - /// Looks up the url location by a [GoRoute]'s name. String namedLocation( String name, { @@ -174,105 +264,6 @@ class RouteConfiguration { .toString(); } - static Set> _debugAllNavigatorsRecursively( - List routes) { - return routes.expand((RouteBase e) { - if (e is GoRoute && e.parentNavigatorKey != null) { - return >{ - e.parentNavigatorKey!, - ..._debugAllNavigatorsRecursively(e.routes) - }; - } else if (e is ShellRoute) { - return >{ - e.navigatorKey, - ..._debugAllNavigatorsRecursively(e.routes) - }; - } else { - return _debugAllNavigatorsRecursively(e.routes); - } - }).toSet(); - } - - /// Validates the branches of a [StatefulShellRoute]. - bool debugValidateStatefulShellBranches( - StatefulShellRoute shellRoute, List branches) { - assert(() { - final Set> uniqueBranchNavigatorKeys = - branches.map((StatefulShellBranch e) => e.navigatorKey).toSet(); - assert(uniqueBranchNavigatorKeys.length == branches.length, - 'StatefulShellRoute must not uses duplicate Navigator keys for the branches'); - - final Set uniqueDefaultLocations = - branches.map((StatefulShellBranch e) => e.defaultLocation).toSet(); - assert(uniqueDefaultLocations.length == branches.length, - 'StatefulShellRoute must not uses duplicate defaultLocations for the branches'); - - assert( - _debugAllNavigatorKeys - .intersection(uniqueBranchNavigatorKeys) - .isEmpty, - 'StatefulShellBranch Navigator key must be unique'); - - // Check to see that the configured defaultLocation of - // StatefulShellBranches points to a descendant route of the route branch. - void checkBranchDefaultLocations( - List routes, RouteMatcher matcher) { - try { - for (final RouteBase route in routes) { - if (route is StatefulShellRoute) { - for (final StatefulShellBranch branch in branches) { - final String defaultLocation = branch.defaultLocation; - final RouteBase defaultLocationRoute = - matcher.findMatch(defaultLocation).last.route; - final RouteBase? match = shellRoute.routes.firstWhereOrNull( - (RouteBase e) => _debugIsDescendantOrSame( - ancestor: e, route: defaultLocationRoute)); - assert( - match != null, - 'The defaultLocation (${branch.defaultLocation}) of ' - 'StatefulShellBranch must match a descendant route of the ' - 'StatefulShellRoute'); - } - } - checkBranchDefaultLocations(route.routes, matcher); - } - } on MatcherError catch (e) { - assert( - false, - 'defaultLocation (${e.location}) of StatefulShellBranch must ' - 'be a valid location'); - } - } - - checkBranchDefaultLocations(routes, RouteMatcher(this)); - - return true; - }()); - return true; - } - - /// Tests if a route is a descendant of, or same as, an ancestor route. - bool _debugIsDescendantOrSame( - {required RouteBase ancestor, required RouteBase route}) { - return _debugAncestorsForRoute(route, routes).contains(ancestor); - } - - static List _debugAncestorsForRoute( - RouteBase targetRoute, List routes) { - for (final RouteBase route in routes) { - if (route.routes.contains(targetRoute)) { - return [route, targetRoute]; - } else { - final List ancestors = - _debugAncestorsForRoute(targetRoute, route.routes); - if (ancestors.isNotEmpty) { - return [route, ...ancestors]; - } - } - } - return []; - } - @override String toString() { return 'RouterConfiguration: $routes'; diff --git a/packages/go_router/lib/src/delegate.dart b/packages/go_router/lib/src/delegate.dart index bc6863721af1..f2ebda3a5dbf 100644 --- a/packages/go_router/lib/src/delegate.dart +++ b/packages/go_router/lib/src/delegate.dart @@ -206,6 +206,7 @@ class _NavigatorStateIterator extends Iterator { if (index < 0) { return false; } + late RouteBase subRoute; for (index -= 1; index >= 0; index -= 1) { final RouteMatch match = matchList.matches[index]; final RouteBase route = match.route; @@ -246,15 +247,8 @@ class _NavigatorStateIterator extends Iterator { } else if (route is ShellRouteBase) { // Must have a ModalRoute parent because the navigator ShellRoute // created must not be the root navigator. - final GlobalKey navigatorKey; - if (route is ShellRoute) { - navigatorKey = route.navigatorKey; - } else if (route is StatefulShellRoute) { - navigatorKey = route.currentBranch!.navigatorKey; - } else { - continue; - } - + final GlobalKey navigatorKey = + route.navigatorKeyForSubRoute(subRoute); final ModalRoute parentModalRoute = ModalRoute.of(navigatorKey.currentContext!)!; // There may be pageless route on top of ModalRoute that the @@ -265,6 +259,7 @@ class _NavigatorStateIterator extends Iterator { current = navigatorKey.currentState!; return true; } + subRoute = route; } assert(index == -1); current = root; diff --git a/packages/go_router/lib/src/misc/stateful_navigation_shell.dart b/packages/go_router/lib/src/misc/stateful_navigation_shell.dart index 79b30bd8cdaa..14371612991b 100644 --- a/packages/go_router/lib/src/misc/stateful_navigation_shell.dart +++ b/packages/go_router/lib/src/misc/stateful_navigation_shell.dart @@ -32,9 +32,6 @@ class InheritedStatefulNavigationShell extends InheritedWidget { } } -/// Builder function for Navigator of the current branch. -typedef BranchNavigatorBuilder = Navigator Function(); - /// Builder function for preloading a route branch navigator. typedef BranchNavigatorPreloadBuilder = Navigator Function( BuildContext context, @@ -65,11 +62,9 @@ class StatefulNavigationShell extends StatefulWidget { required this.configuration, required this.shellRoute, required this.shellGoRouterState, - required this.branches, - required this.currentBranch, - required this.currentNavigatorBuilder, + required this.currentNavigator, required this.currentMatchList, - required this.branchNavigatorBuilder, + required this.branchPreloadNavigatorBuilder, super.key, }); @@ -82,20 +77,14 @@ class StatefulNavigationShell extends StatefulWidget { /// The [GoRouterState] for the navigation shell. final GoRouterState shellGoRouterState; - /// The currently active set of [StatefulShellBranch]s. - final List branches; - - /// The [StatefulShellBranch] for the current location - final StatefulShellBranch currentBranch; - - /// The builder for the navigator of the currently active route branch - final BranchNavigatorBuilder currentNavigatorBuilder; + /// The navigator for the currently active route branch + final Navigator currentNavigator; /// The RouteMatchList for the current location final UnmodifiableRouteMatchList currentMatchList; /// Builder for route branch navigators (used for preloading). - final BranchNavigatorPreloadBuilder branchNavigatorBuilder; + final BranchNavigatorPreloadBuilder branchPreloadNavigatorBuilder; @override State createState() => StatefulNavigationShellState(); @@ -107,6 +96,8 @@ class StatefulNavigationShellState extends State { late StatefulShellRouteState _routeState; + List get _branches => widget.shellRoute.branches; + Navigator? _navigatorForBranch(StatefulShellBranch branch) { return _navigatorCache[branch.navigatorKey]; } @@ -116,13 +107,13 @@ class StatefulNavigationShellState extends State { } int _findCurrentIndex() { - final int index = widget.branches.indexWhere((StatefulShellBranch e) => - e.navigatorKey == widget.currentBranch.navigatorKey); + final int index = _branches.indexWhere((StatefulShellBranch e) => + e.navigatorKey == widget.currentNavigator.key); assert(index >= 0); return index; } - void _switchActiveBranch(StatefulShellBranchState navigatorState, + void _switchActiveBranch(StatefulShellBranchState branchState, UnmodifiableRouteMatchList? unmodifiableRouteMatchList) { final GoRouter goRouter = GoRouter.of(context); final RouteMatchList? matchList = @@ -133,10 +124,31 @@ class StatefulNavigationShellState extends State { .then( (RouteMatchList matchList) => goRouter.routerDelegate.setNewRoutePath(matchList), - onError: (_) => goRouter.go(navigatorState.branch.defaultLocation), + onError: (_) => goRouter.go(_defaultBranchLocation(branchState)), ); } else { - goRouter.go(navigatorState.branch.defaultLocation); + goRouter.go(_defaultBranchLocation(branchState)); + } + } + + String _defaultBranchLocation(StatefulShellBranchState branchState) { + String? defaultLocation = branchState.branch.defaultLocation; + defaultLocation ??= widget.configuration + .findShellRouteBranchDefaultLocation(branchState.branch); + return defaultLocation; + } + + void _preloadBranches() { + final List states = _routeState.branchStates; + for (StatefulShellBranchState state in states) { + if (state.branch.preload && !state.isLoaded) { + state = _updateStatefulShellBranchState(state, loaded: true); + _preloadBranch(state).then((StatefulShellBranchState navigatorState) { + setState(() { + _updateRouteBranchState(navigatorState); + }); + }); + } } } @@ -148,7 +160,7 @@ class StatefulNavigationShellState extends State { GoRouter.of(context).routeInformationParser; final Future routeMatchList = parser.parseRouteInformationWithDependencies( - RouteInformation(location: branchState.branch.defaultLocation), + RouteInformation(location: _defaultBranchLocation(branchState)), context); StatefulShellBranchState createBranchNavigator(RouteMatchList matchList) { @@ -158,20 +170,22 @@ class StatefulNavigationShellState extends State { .indexWhere((RouteMatch e) => e.route == widget.shellRoute); // Keep only the routes from and below the root route in the match list and // use that to build the Navigator for the branch - BranchNavigatorBuilder? navigatorBuilder; + Navigator? navigator; if (shellRouteIndex >= 0 && shellRouteIndex < (matchList.matches.length - 1)) { - navigatorBuilder = () => widget.branchNavigatorBuilder( - context, - matchList, - shellRouteIndex + 1, - branch.navigatorKey, - branch.restorationScopeId, - ); + navigator = widget.branchPreloadNavigatorBuilder( + context, + matchList, + shellRouteIndex + 1, + branch.navigatorKey, + branch.restorationScopeId, + ); } - return _copyStatefulShellBranchState(branchState, - navigatorBuilder: navigatorBuilder, - matchList: matchList.unmodifiableRouteMatchList()); + return _updateStatefulShellBranchState( + branchState, + navigator: navigator, + matchList: matchList.unmodifiableRouteMatchList(), + ); } return routeMatchList.then(createBranchNavigator); @@ -179,7 +193,6 @@ class StatefulNavigationShellState extends State { void _updateRouteBranchState(StatefulShellBranchState branchState, {int? currentIndex}) { - final List branches = widget.branches; final List existingStates = _routeState.branchStates; final List newStates = @@ -187,7 +200,7 @@ class StatefulNavigationShellState extends State { // Build a new list of the current StatefulShellBranchStates, with an // updated state for the current branch etc. - for (final StatefulShellBranch branch in branches) { + for (final StatefulShellBranch branch in _branches) { if (branch.navigatorKey == branchState.navigatorKey) { newStates.add(branchState); } else { @@ -199,7 +212,7 @@ class StatefulNavigationShellState extends State { // Remove any obsolete cached Navigators final Set validKeys = - branches.map((StatefulShellBranch e) => e.navigatorKey).toSet(); + _branches.map((StatefulShellBranch e) => e.navigatorKey).toSet(); _navigatorCache.removeWhere((Key key, _) => !validKeys.contains(key)); _routeState = _routeState.copy( @@ -208,40 +221,30 @@ class StatefulNavigationShellState extends State { ); } - void _preloadBranches() { - final List states = _routeState.branchStates; - for (StatefulShellBranchState state in states) { - if (state.branch.preload && !state.preloading) { - state = _copyStatefulShellBranchState(state, loaded: true); - _preloadBranch(state).then((StatefulShellBranchState navigatorState) { - setState(() { - _updateRouteBranchState(navigatorState); - }); - }); - } - } - } - void _updateRouteStateFromWidget() { final int index = _findCurrentIndex(); - final StatefulShellBranch branch = widget.currentBranch; + final StatefulShellBranch branch = _branches[index]; // Update or create a new StatefulShellBranchState for the current branch // (i.e. the arguments currently provided to the Widget). - StatefulShellBranchState? existingState = _routeState.branchStates + StatefulShellBranchState? currentBranchState = _routeState.branchStates .firstWhereOrNull((StatefulShellBranchState e) => e.branch == branch); - if (existingState != null) { - existingState = _copyStatefulShellBranchState(existingState, - navigatorBuilder: widget.currentNavigatorBuilder, - matchList: widget.currentMatchList); + if (currentBranchState != null) { + currentBranchState = _updateStatefulShellBranchState( + currentBranchState, + navigator: widget.currentNavigator, + matchList: widget.currentMatchList, + ); } else { - existingState = _createStatefulShellBranchState(branch, - navigatorBuilder: widget.currentNavigatorBuilder, - matchList: widget.currentMatchList); + currentBranchState = _createStatefulShellBranchState( + branch, + navigator: widget.currentNavigator, + matchList: widget.currentMatchList, + ); } _updateRouteBranchState( - existingState, + currentBranchState, currentIndex: index, ); @@ -249,36 +252,45 @@ class StatefulNavigationShellState extends State { } void _resetState() { - final StatefulShellBranchState navigatorState = + final StatefulShellBranchState currentBranchState = _routeState.currentBranchState; _navigatorCache.clear(); _setupInitialStatefulShellRouteState(); - GoRouter.of(context).go(navigatorState.branch.defaultLocation); + GoRouter.of(context).go(_defaultBranchLocation(currentBranchState)); } - StatefulShellBranchState _copyStatefulShellBranchState( - StatefulShellBranchState branchState, - {BranchNavigatorBuilder? navigatorBuilder, - UnmodifiableRouteMatchList? matchList, - bool? loaded}) { - bool dirty = branchState.matchList != matchList; - if (navigatorBuilder != null) { - final Navigator? existingNav = _navigatorForBranch(branchState.branch); - if (existingNav == null || dirty) { + StatefulShellBranchState _updateStatefulShellBranchState( + StatefulShellBranchState branchState, { + Navigator? navigator, + UnmodifiableRouteMatchList? matchList, + bool? loaded, + }) { + bool dirty = false; + if (matchList != null) { + dirty = branchState.matchList != matchList; + } + + if (navigator != null) { + // Only update Navigator for branch if matchList is different (i.e. + // dirty == true) or if Navigator didn't already exist + final bool hasExistingNav = + _navigatorForBranch(branchState.branch) != null; + if (!hasExistingNav || dirty) { dirty = true; - _setNavigatorForBranch(branchState.branch, navigatorBuilder()); + _setNavigatorForBranch(branchState.branch, navigator); } } - final _BranchNavigatorProxy branchWidget = - branchState.child as _BranchNavigatorProxy; final bool isLoaded = loaded ?? _navigatorForBranch(branchState.branch) != null; - dirty = dirty || isLoaded != branchWidget.loaded; + dirty = dirty || isLoaded != branchState.isLoaded; if (dirty) { return branchState.copy( - child: branchWidget.copy(loaded: isLoaded), + child: _BranchNavigatorProxy( + branch: branchState.branch, + navigatorForBranch: _navigatorForBranch), + isLoaded: isLoaded, matchList: matchList, ); } else { @@ -288,23 +300,21 @@ class StatefulNavigationShellState extends State { StatefulShellBranchState _createStatefulShellBranchState( StatefulShellBranch branch, - {BranchNavigatorBuilder? navigatorBuilder, + {Navigator? navigator, UnmodifiableRouteMatchList? matchList}) { - if (navigatorBuilder != null) { - _setNavigatorForBranch(branch, navigatorBuilder()); + if (navigator != null) { + _setNavigatorForBranch(branch, navigator); } return StatefulShellBranchState( branch: branch, child: _BranchNavigatorProxy( - branch: branch, - navigatorForBranch: _navigatorForBranch, - ), + branch: branch, navigatorForBranch: _navigatorForBranch), matchList: matchList, ); } void _setupInitialStatefulShellRouteState() { - final List states = widget.branches + final List states = _branches .map((StatefulShellBranch e) => _createStatefulShellBranchState(e)) .toList(); @@ -358,24 +368,13 @@ typedef _NavigatorForBranch = Navigator? Function(StatefulShellBranch); /// Widget that serves as the proxy for a branch Navigator Widget, which /// possibly hasn't been created yet. class _BranchNavigatorProxy extends StatelessWidget { - const _BranchNavigatorProxy( - {required this.branch, - required this.navigatorForBranch, - this.loaded = false, - super.key}); - - _BranchNavigatorProxy copy({bool? loaded}) { - return _BranchNavigatorProxy( - branch: branch, - loaded: loaded ?? this.loaded, - navigatorForBranch: navigatorForBranch, - key: key, - ); - } + const _BranchNavigatorProxy({ + required this.branch, + required this.navigatorForBranch, + }); final StatefulShellBranch branch; final _NavigatorForBranch navigatorForBranch; - final bool loaded; @override Widget build(BuildContext context) { @@ -392,15 +391,12 @@ class _IndexedStackedRouteBranchContainer extends StatelessWidget { @override Widget build(BuildContext context) { - final StatefulShellBranchState currentState = routeState.currentBranchState; - final List states = routeState.branchStates; - final List children = states - .map((StatefulShellBranchState e) => - _buildRouteBranchContainer(context, e == currentState, e)) + final int currentIndex = routeState.currentIndex; + final List children = routeState.branchStates + .mapIndexed((int index, StatefulShellBranchState item) => + _buildRouteBranchContainer(context, currentIndex == index, item)) .toList(); - final int currentIndex = - states.indexWhere((StatefulShellBranchState e) => e == currentState); return IndexedStack(index: currentIndex, children: children); } @@ -418,5 +414,4 @@ class _IndexedStackedRouteBranchContainer extends StatelessWidget { extension _StatefulShellBranchStateHelper on StatefulShellBranchState { GlobalKey get navigatorKey => branch.navigatorKey; - bool get preloading => (child as _BranchNavigatorProxy).loaded; } diff --git a/packages/go_router/lib/src/route.dart b/packages/go_router/lib/src/route.dart index e3b8a2dbc4db..ae003ace168f 100644 --- a/packages/go_router/lib/src/route.dart +++ b/packages/go_router/lib/src/route.dart @@ -3,12 +3,10 @@ // found in the LICENSE file. import 'package:collection/collection.dart'; -import 'package:flutter/foundation.dart'; import 'package:flutter/widgets.dart'; import 'package:meta/meta.dart'; import 'configuration.dart'; -import 'misc/stateful_navigation_shell.dart'; import 'pages/custom_transition_page.dart'; import 'path_utils.dart'; import 'typedefs.dart'; @@ -344,6 +342,10 @@ abstract class ShellRouteBase extends RouteBase { /// matching sub-routes. Typically, a shell route builds its shell around this /// Widget. final ShellRoutePageBuilder? pageBuilder; + + /// 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); } /// A route that displays a UI shell around the matching child route. @@ -468,6 +470,12 @@ class ShellRoute extends ShellRouteBase { /// Restoration ID to save and restore the state of the navigator, including /// its history. final String? restorationScopeId; + + @override + GlobalKey navigatorKeyForSubRoute(RouteBase subRoute) { + assert(routes.contains(subRoute)); + return navigatorKey; + } } /// A route that displays a UI shell with separate [Navigator]s for its @@ -481,55 +489,54 @@ class ShellRoute extends ShellRouteBase { /// implementing a UI with a [BottomNavigationBar], with a persistent navigation /// state for each tab. /// -/// A StatefulShellRoute is created by providing a List of [StatefulShellBranch] -/// items, each representing a separate stateful branch in the route tree. The -/// branches can be provided either statically, by passing a list of branches in -/// the constructor, or dynamically by instead providing a [branchBuilder]. -/// StatefulShellBranch defines the root location(s) of the branch, as well as -/// the Navigator key ([GlobalKey]) for the Navigator associated with the -/// branch. +/// A StatefulShellRoute is created by specifying a List of [ShellRouteBranch] +/// items, each representing a separate stateful branch in the route tree. +/// ShellRouteBranch provides the root routes and the Navigator key ([GlobalKey]) +/// for the branch, as well as an optional default location. /// /// Like [ShellRoute], you can provide a [builder] and [pageBuilder] when /// creating a StatefulShellRoute. However, StatefulShellRoute differs in that -/// the builder is mandatory and the pageBuilder will be used in addition to the -/// builder. The child parameters of the builders are also a bit different, even -/// though this should normally not affect how you implemented the builders. -/// -/// For the pageBuilder, the child parameter will simply be the stateful shell -/// already built for this route, using the builder function. In the builder -/// function however, the child parameter is a Widget that contains - and is +/// the builder is mandatory and the pageBuilder may optionally be used in +/// addition to the builder. The reason for this is that the builder function is +/// not used to generate the final Widget used to represent the +/// StatefulShellRoute. Instead, the returned Widget will only form part of the +/// stateful navigation shell for this route. This means that the role of the +/// [builder] is to provide part of the navigation shell, whereas the role of +/// [pageBuilder] is simply to customize the Page used for this route (see +/// example below). +/// +/// In the builder function, the child parameter is a Widget that contains - and is /// responsible for managing - the Navigators for the different route branches /// of this StatefulShellRoute. This widget is meant to be used as the body of a /// custom shell implementation, for example as the body of [Scaffold] with a /// [BottomNavigationBar]. /// -/// The builder function of a StatefulShellRoute will be invoked from within a -/// wrapper Widget that provides access to the current [StatefulShellRouteState] -/// associated with the route (via the method [StatefulShellRoute.of]). That -/// state object exposes information such as the current branch index, the state -/// of the route branches etc. It is also with the help this state object you -/// can change the active branch, i.e. restore the navigation stack of another -/// branch. This is accomplished using the method -/// [StatefulShellRouteState.goBranch], and providing either a Navigator key, -/// branch name or branch index. For example: +/// The state of a StatefulShellRoute is represented by +/// [StatefulShellRouteState], which can be accessed by calling +/// [StatefulShellRouteState.of]. This state object exposes information such +/// as the current branch index, the state of the route branches etc. The state +/// object also provides support for changing the active branch, i.e. restoring +/// the navigation stack of another branch. This is accomplished using the +/// method [StatefulShellRouteState.goBranch], and providing either a Navigator +/// key, branch name or branch index. For example: /// /// ``` /// void _onBottomNavigationBarItemTapped(BuildContext context, int index) { -/// final StatefulShellRouteState shellState = StatefulShellRoute.of(context); +/// final StatefulShellRouteState shellState = StatefulShellRouteState.of(context); /// shellState.goBranch(index: index); /// } /// ``` /// -/// Sometimes you need greater control over the layout and animations of the +/// Sometimes greater control is needed over the layout and animations of the /// Widgets representing the branch Navigators. In such cases, the child /// argument in the builder function can be ignored, and a custom implementation /// can instead be built using the child widgets of the branches /// (see [StatefulShellRouteState.children]) directly. For example: /// /// ``` -/// final StatefulShellRouteState shellState = StatefulShellRoute.of(context); +/// final StatefulShellRouteState shellState = StatefulShellRouteState.of(context); /// final int currentIndex = shellState.currentIndex; -/// final List children = shellRouteState.children; +/// final List children = shellRouteState.children; /// return MyCustomShell(currentIndex, children); /// ``` /// @@ -541,49 +548,59 @@ class ShellRoute extends ShellRouteBase { /// of the builder function. /// /// ``` +/// final GlobalKey _tabANavigatorKey = +/// GlobalKey(debugLabel: 'tabANavigator'); +/// final GlobalKey _tabBNavigatorKey = +/// GlobalKey(debugLabel: 'tabBNavigator'); +/// /// final GoRouter _router = GoRouter( /// initialLocation: '/a', /// routes: [ /// StatefulShellRoute( -/// routes: [ -/// GoRoute( -/// /// The screen to display as the root in the first tab of the -/// /// bottom navigation bar. -/// path: '/a', -/// builder: (BuildContext context, GoRouterState state) => -/// const RootScreen(label: 'A', detailsPath: '/a/details'), +/// builder: (BuildContext context, GoRouterState state, +/// Widget navigatorContainer) { +/// return ScaffoldWithNavBar(body: navigatorContainer); +/// }, +/// branches: [ +/// /// The first branch, i.e. tab 'A' +/// ShellRouteBranch( +/// navigatorKey: _tabANavigatorKey, /// routes: [ -/// /// Will cover screen A but not the bottom navigation bar /// GoRoute( -/// path: 'details', +/// path: '/a', /// builder: (BuildContext context, GoRouterState state) => -/// const DetailsScreen(label: 'A'), +/// const RootScreen(label: 'A', detailsPath: '/a/details'), +/// routes: [ +/// /// Will cover screen A but not the bottom navigation bar +/// GoRoute( +/// path: 'details', +/// builder: (BuildContext context, GoRouterState state) => +/// const DetailsScreen(label: 'A'), +/// ), +/// ], /// ), /// ], /// ), -/// GoRoute( -/// /// The screen to display as the root in the second tab of the -/// /// bottom navigation bar. -/// path: '/b', -/// builder: (BuildContext context, GoRouterState state) => -/// const RootScreen(label: 'B', detailsPath: '/b/details'), +/// /// The second branch, i.e. tab 'B' +/// ShellRouteBranch( +/// navigatorKey: _tabBNavigatorKey, /// routes: [ -/// /// Will cover screen B but not the bottom navigation bar /// GoRoute( -/// path: 'details', +/// path: '/b', /// builder: (BuildContext context, GoRouterState state) => -/// const DetailsScreen(label: 'B'), +/// const RootScreen(label: 'B', detailsPath: '/b/details'), +/// routes: [ +/// /// Will cover screen B but not the bottom navigation bar +/// GoRoute( +/// path: 'details', +/// builder: (BuildContext context, GoRouterState state) => +/// const DetailsScreen(label: 'B'), +/// ), +/// ], /// ), /// ], /// ), /// ], -/// branches: [ -/// StatefulShellBranch(rootLocation: '/a'), -/// StatefulShellBranch(rootLocation: '/b'), -/// ], -/// builder: (BuildContext context, GoRouterState state, Widget child) { -/// return ScaffoldWithNavBar(body: child); -/// }, /// ), /// ], /// ); @@ -600,34 +617,34 @@ class ShellRoute extends ShellRouteBase { /// initialLocation: '/a', /// routes: [ /// StatefulShellRoute( -/// routes: [ -/// GoRoute( -/// /// The screen to display as the root in the first tab of the -/// /// bottom navigation bar. -/// path: '/a', -/// builder: (BuildContext context, GoRouterState state) => +/// builder: (BuildContext context, GoRouterState state, +/// Widget navigationContainer) { +/// return ScaffoldWithNavBar(body: navigationContainer); +/// }, +/// pageBuilder: +/// (BuildContext context, GoRouterState state, Widget statefulShell) { +/// return NoTransitionPage(child: statefulShell); +/// }, +/// branches: [ +/// /// The first branch, i.e. root of tab 'A' +/// ShellRouteBranch(routes: [ +/// GoRoute( +/// parentNavigatorKey: _tabANavigatorKey, +/// path: '/a', +/// builder: (BuildContext context, GoRouterState state) => /// const RootScreen(label: 'A', detailsPath: '/a/details'), -/// ), -/// GoRoute( -/// /// The screen to display as the root in the second tab of the -/// /// bottom navigation bar. -/// path: '/b', -/// builder: (BuildContext context, GoRouterState state) => -/// const RootScreen(label: 'B', detailsPath: '/b/details'), -/// ), -/// ], -/// /// To enable a dynamic set of StatefulShellBranches (and thus -/// /// Navigators), use 'branchBuilder' instead of 'branches'. -/// branchBuilder: (BuildContext context, GoRouterState state) => -/// [ -/// StatefulShellBranch(rootLocation: '/a'), -/// StatefulShellBranch(rootLocation: '/b'), +/// ), +/// ]), +/// /// The second branch, i.e. root of tab 'B' +/// ShellRouteBranch(routes: [ +/// GoRoute( +/// parentNavigatorKey: _tabBNavigatorKey, +/// path: '/b', +/// builder: (BuildContext context, GoRouterState state) => +/// const RootScreen(label: 'B', detailsPath: '/b/details/1'), +/// ), +/// ]), /// ], -/// builder: (BuildContext context, GoRouterState state, Widget child) => -/// ScaffoldWithNavBar(body: child), -/// pageBuilder: -/// (BuildContext context, GoRouterState state, Widget statefulShell) => -/// NoTransitionPage(child: statefulShell), /// ), /// ], /// ); @@ -635,19 +652,17 @@ class ShellRoute extends ShellRouteBase { /// /// To access the current state of this route, to for instance access the /// index of the current route branch - use the method -/// [StatefulShellRoute.of]. For example: +/// [StatefulShellRouteState.of]. For example: /// /// ``` -/// final StatefulShellRouteState shellState = StatefulShellRoute.of(context); +/// final StatefulShellRouteState shellState = StatefulShellRouteState.of(context); /// ``` /// /// See [Stateful Nested Navigation](https://github.com/flutter/packages/blob/main/packages/go_router/example/lib/stateful_nested_navigation.dart) /// for a complete runnable example using StatefulShellRoute. -/// For an example of the use of dynamic branches, see -/// [Dynamic Stateful Nested Navigation](https://github.com/flutter/packages/blob/main/packages/go_router/example/lib/dynamic_stateful_shell_branches.dart). class StatefulShellRoute extends ShellRouteBase { - /// Constructs a [StatefulShellRoute] from a list of [StatefulShellBranch], each - /// representing a root in a stateful route branch. + /// Constructs a [StatefulShellRoute] from a list of [StatefulShellBranch]es, + /// each representing a separate nested navigation tree (branch). /// /// A separate [Navigator] will be created for each of the branches, using /// the navigator key specified in [StatefulShellBranch]. Note that unlike @@ -655,68 +670,71 @@ class StatefulShellRoute extends ShellRouteBase { /// a StatefulShellRoute. The pageBuilder however is optional, and is used /// in addition to the builder. StatefulShellRoute({ - required super.routes, + required this.branches, required super.builder, - StatefulShellBranchBuilder? branchBuilder, - List? branches, super.pageBuilder, - }) : assert(branchBuilder != null || branches != null), - branchBuilder = branchBuilder ?? _builderFromBranches(branches!), - super._() { + }) : assert(branches.isNotEmpty), + assert(_debugUniqueNavigatorKeys(branches).length == branches.length, + 'Navigator keys must be unique'), + super._(routes: _routes(branches)) { for (int i = 0; i < routes.length; ++i) { final RouteBase route = routes[i]; if (route is GoRoute) { - assert(route.parentNavigatorKey == null); + assert(route.parentNavigatorKey == null || + route.parentNavigatorKey == branches[i].navigatorKey); } } } - static StatefulShellBranchBuilder _builderFromBranches( - List branches) { - return (_, __) => branches; + /// Representations of the different stateful route branches that this + /// shell route will manage. + /// + /// Each branch uses a separate [Navigator], identified + /// [StatefulShellBranch.navigatorKey]. + final List branches; + + @override + GlobalKey navigatorKeyForSubRoute(RouteBase subRoute) { + return branchForSubRoute(subRoute).navigatorKey; } - /// The navigation branch builder for this shell route. - /// - /// This builder is used to provide the currently active StatefulShellBranches - /// at any point in time. Each branch uses a separate [Navigator], identified - /// by [StatefulShellBranch.navigatorKey]. - final StatefulShellBranchBuilder branchBuilder; - - /// Gets the state for the nearest stateful shell route in the Widget tree. - static StatefulShellRouteState of(BuildContext context) { - final InheritedStatefulNavigationShell? inherited = context - .dependOnInheritedWidgetOfExactType(); - assert(inherited != null, - 'No InheritedStatefulNavigationShell found in context'); - return inherited!.routeState; + /// Returns the StatefulShellBranch that is to be used for the specified + /// immediate sub-route of this shell route. + StatefulShellBranch branchForSubRoute(RouteBase subRoute) { + final StatefulShellBranch? branch = branches.firstWhereOrNull( + (StatefulShellBranch e) => e.routes.contains(subRoute)); + assert(branch != null); + return branch!; } + + static List _routes(List branches) => + branches.expand((StatefulShellBranch e) => e.routes).toList(); + + static Set> _debugUniqueNavigatorKeys( + List branches) => + Set>.from( + branches.map((StatefulShellBranch e) => e.navigatorKey)); } -/// Representation of a separate navigation branch in a [StatefulShellRoute]. +/// Representation of a separate branch in a stateful navigation tree, used to +/// configure [StatefulShellRoute]. /// -/// The only required argument is the rootLocation (or [rootLocations]), which -/// identify the [defaultLocation] to be used when loading the branch for the -/// first time (for instance when switching branch using the goBranch method in -/// [StatefulShellBranchState]). The rootLocations also identify the valid root -/// locations for a particular StatefulShellBranch, and thus on which Navigator -/// those routes should be placed on. -/// -/// A [navigatorKey] is optional, but can be useful to provide in case you need +/// The only required argument when creating a ShellRouteBranch is the +/// sub-routes ([routes]), however in some cases you may also need to specify +/// the [defaultLocation], for instance of you're using another shell route as +/// direct sub-route. A [navigatorKey] can be useful to provide in case you need /// to use the [Navigator] created for this branch elsewhere. @immutable class StatefulShellBranch { /// Constructs a [StatefulShellBranch]. StatefulShellBranch({ + required this.routes, GlobalKey? navigatorKey, - List? rootLocations, - String? rootLocation, + this.defaultLocation, this.name, this.restorationScopeId, this.preload = false, - }) : assert(rootLocation != null || (rootLocations?.isNotEmpty ?? false)), - rootLocations = rootLocations ?? [rootLocation!], - navigatorKey = navigatorKey ?? + }) : navigatorKey = navigatorKey ?? GlobalKey( debugLabel: name != null ? 'Branch-$name' : null); @@ -728,8 +746,14 @@ class StatefulShellBranch { /// Navigator instead of the root Navigator. final GlobalKey navigatorKey; - /// The valid root locations for this branch. - final List rootLocations; + /// The list of child routes associated with this route branch. + final List routes; + + /// The default location for this route branch. + /// + /// If none is specified, the first descendant [GoRoute] will be used (i.e. + /// first element in [routes], or a descendant). + final String? defaultLocation; /// An optional name for this branch. final String? name; @@ -738,30 +762,17 @@ class StatefulShellBranch { /// [StatefulShellRoute] is visited for the first time. /// /// If this is true, this branch will be preloaded by navigating to - /// the root location (first entry in [rootLocations]). + /// the default location (see [defaultLocation]). 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; /// Restoration ID to save and restore the state of the navigator, including /// its history. final String? restorationScopeId; - /// Returns the default location for this branch (by default the first - /// entry in [rootLocations]). - String get defaultLocation => rootLocations.first; - - /// Checks if this branch is intended to be used for the provided - /// GoRouterState. - bool isBranchFor(GoRouterState state) { - final String? match = rootLocations - .firstWhereOrNull((String e) => state.location.startsWith(e)); - return match != null; - } - - /// Gets the state for the current branch of the nearest stateful shell route - /// in the Widget tree. - static StatefulShellBranchState of(BuildContext context) => - StatefulShellRoute.of(context).currentBranchState; - @override bool operator ==(Object other) { if (identical(other, this)) { @@ -771,7 +782,7 @@ class StatefulShellBranch { return false; } return other.navigatorKey == navigatorKey && - listEquals(other.rootLocations, rootLocations) && + other.defaultLocation == defaultLocation && other.name == name && other.preload == preload && other.restorationScopeId == restorationScopeId; @@ -779,28 +790,5 @@ class StatefulShellBranch { @override int get hashCode => Object.hash( - navigatorKey, rootLocations, name, preload, restorationScopeId); -} - -/// StatefulShellRoute extension that provides support for resolving the -/// current StatefulShellBranch. -/// -/// Should not be used directly, consider using [StatefulShellRoute.of] or -/// [StatefulShellBranch.of] to access [StatefulShellBranchState] for the -/// current context. -extension StatefulShellBranchResolver on StatefulShellRoute { - static final Expando _shellBranchCache = - Expando(); - - /// The current StatefulShellBranch, previously resolved using [resolveBranch]. - StatefulShellBranch? get currentBranch => _shellBranchCache[this]; - - /// Resolves the current StatefulShellBranch, given the provided GoRouterState. - StatefulShellBranch? resolveBranch( - List branches, GoRouterState state) { - final StatefulShellBranch? branch = branches - .firstWhereOrNull((StatefulShellBranch e) => e.isBranchFor(state)); - _shellBranchCache[this] = branch; - return branch; - } + navigatorKey, defaultLocation, name, preload, restorationScopeId); } diff --git a/packages/go_router/lib/src/shell_state.dart b/packages/go_router/lib/src/shell_state.dart new file mode 100644 index 000000000000..330751dcf405 --- /dev/null +++ b/packages/go_router/lib/src/shell_state.dart @@ -0,0 +1,233 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:collection/collection.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/widgets.dart'; + +import '../go_router.dart'; +import 'matching.dart'; +import 'misc/errors.dart'; +import 'misc/stateful_navigation_shell.dart'; + +/// The snapshot of the current state of a [StatefulShellRoute]. +/// +/// Note that this an immutable class, that represents the snapshot of the state +/// of a StatefulShellRoute at a given point in time. Therefore, instances of +/// this object should not be stored, but instead fetched fresh when needed, +/// using the method [StatefulShellRouteState.of]. +@immutable +class StatefulShellRouteState { + /// Constructs a [StatefulShellRouteState]. + const StatefulShellRouteState({ + required this.route, + required this.branchStates, + required this.currentIndex, + required void Function( + StatefulShellBranchState, UnmodifiableRouteMatchList?) + switchActiveBranch, + required void Function() resetState, + }) : _switchActiveBranch = switchActiveBranch, + _resetState = resetState; + + /// Constructs a copy of this [StatefulShellRouteState], with updated values + /// for some of the fields. + StatefulShellRouteState copy( + {List? branchStates, int? currentIndex}) { + return StatefulShellRouteState( + route: route, + branchStates: branchStates ?? this.branchStates, + currentIndex: currentIndex ?? this.currentIndex, + switchActiveBranch: _switchActiveBranch, + resetState: _resetState, + ); + } + + /// The associated [StatefulShellRoute] + final StatefulShellRoute route; + + /// The state for all separate route branches associated with a + /// [StatefulShellRoute]. + final List branchStates; + + /// The state associated with the current [StatefulShellBranch]. + StatefulShellBranchState get currentBranchState => branchStates[currentIndex]; + + /// The index of the currently active [StatefulShellBranch]. + /// + /// Corresponds to the index of the branch in the List returned from + /// branchBuilder of [StatefulShellRoute]. + final int currentIndex; + + /// The Navigator key of the current navigator. + GlobalKey get currentNavigatorKey => + currentBranchState.branch.navigatorKey; + + final void Function(StatefulShellBranchState, UnmodifiableRouteMatchList?) + _switchActiveBranch; + + final void Function() _resetState; + + /// Gets the [Widget]s representing each of the shell branches. + /// + /// The Widget returned from this method contains the [Navigator]s of the + /// branches. Note that the Widgets returned by this method should only be + /// added to the widget tree if using a custom branch container Widget + /// implementation, where the child parameter in the [ShellRouteBuilder] of + /// the [StatefulShellRoute] is ignored (i.e. not added to the widget tree). + /// See [StatefulShellBranchState.child]. + List get children => + branchStates.map((StatefulShellBranchState e) => e.child).toList(); + + /// Navigate to the current location of the shell navigator with the provided + /// Navigator key, name or index. + /// + /// This method will switch the currently active [Navigator] for the + /// [StatefulShellRoute] by replacing the current navigation stack with the + /// one of the route branch identified by the provided Navigator key, name or + /// index. If resetLocation is true, the branch will be reset to its default + /// location (see [StatefulShellBranch.defaultLocation]). + void goBranch({ + GlobalKey? navigatorKey, + String? name, + int? index, + bool resetLocation = false, + }) { + assert(navigatorKey != null || name != null || index != null); + assert([navigatorKey, name, index].whereNotNull().length == 1); + + final StatefulShellBranchState? state; + if (navigatorKey != null) { + state = branchStates.firstWhereOrNull((StatefulShellBranchState e) => + e.branch.navigatorKey == navigatorKey); + if (state == null) { + throw GoError('Unable to find branch with key $navigatorKey'); + } + } else if (name != null) { + state = branchStates.firstWhereOrNull( + (StatefulShellBranchState e) => e.branch.name == name); + if (state == null) { + throw GoError('Unable to find branch with name "$name"'); + } + } else { + state = branchStates[index!]; + } + + _switchActiveBranch(state, resetLocation ? null : state._matchList); + } + + /// Refreshes this StatefulShellRouteState by rebuilding the state for the + /// current location. + void refresh() { + _switchActiveBranch(currentBranchState, currentBranchState._matchList); + } + + /// Resets this StatefulShellRouteState by clearing all navigation state of + /// the branches, and returning the current branch to its default location. + void reset() { + _resetState(); + } + + @override + bool operator ==(Object other) { + if (identical(other, this)) { + return true; + } + if (other is! StatefulShellRouteState) { + return false; + } + return other.route == route && + listEquals(other.branchStates, branchStates) && + other.currentIndex == currentIndex; + } + + @override + int get hashCode => Object.hash(route, currentIndex, currentIndex); + + /// Gets the state for the nearest stateful shell route in the Widget tree. + static StatefulShellRouteState of(BuildContext context) { + final InheritedStatefulNavigationShell? inherited = context + .dependOnInheritedWidgetOfExactType(); + assert(inherited != null, + 'No InheritedStatefulNavigationShell found in context'); + return inherited!.routeState; + } +} + +/// The snapshot of the current state for a particular route branch +/// ([StatefulShellBranch]) in a [StatefulShellRoute]. +/// +/// Note that this an immutable class, that represents the snapshot of the state +/// of a StatefulShellBranchState at a given point in time. Therefore, instances of +/// this object should not be stored, but instead fetched fresh when needed, +/// via the [StatefulShellRouteState] returned by the method +/// [StatefulShellRouteState.of]. +@immutable +class StatefulShellBranchState { + /// Constructs a [StatefulShellBranchState]. + const StatefulShellBranchState({ + required this.branch, + required this.child, + this.isLoaded = false, + UnmodifiableRouteMatchList? matchList, + }) : _matchList = matchList; + + /// Constructs a copy of this [StatefulShellBranchState], with updated values for + /// some of the fields. + StatefulShellBranchState copy( + {Widget? child, bool? isLoaded, UnmodifiableRouteMatchList? matchList}) { + return StatefulShellBranchState( + branch: branch, + child: child ?? this.child, + isLoaded: isLoaded ?? this.isLoaded, + matchList: matchList ?? _matchList, + ); + } + + /// The associated [StatefulShellBranch] + final StatefulShellBranch branch; + + /// The [Widget] representing this route branch in a [StatefulShellRoute]. + /// + /// The Widget returned from this method contains the [Navigator] of the + /// branch. Note that the Widget returned by this method should only + /// be added to the widget tree if using a custom branch container Widget + /// implementation, where the child parameter in the [ShellRouteBuilder] of + /// the [StatefulShellRoute] is ignored (i.e. not added to the widget tree). + final Widget child; + + /// The current navigation stack for the branch. + final UnmodifiableRouteMatchList? _matchList; + + /// Returns true if this branch has been loaded (i.e. visited once or + /// pre-loaded). + final bool isLoaded; + + @override + bool operator ==(Object other) { + if (identical(other, this)) { + return true; + } + if (other is! StatefulShellBranchState) { + return false; + } + return other.branch == branch && + other.child == child && + other._matchList == _matchList; + } + + @override + int get hashCode => Object.hash(branch, child, _matchList); + + /// Gets the state for the current branch of the nearest stateful shell route + /// in the Widget tree. + static StatefulShellBranchState of(BuildContext context) => + StatefulShellRouteState.of(context).currentBranchState; +} + +/// Helper extension on [StatefulShellBranchState], for internal use. +extension StatefulShellBranchStateHelper on StatefulShellBranchState { + /// The current navigation stack for the branch. + UnmodifiableRouteMatchList? get matchList => _matchList; +} diff --git a/packages/go_router/lib/src/state.dart b/packages/go_router/lib/src/state.dart index 163a159568b7..721565a5e626 100644 --- a/packages/go_router/lib/src/state.dart +++ b/packages/go_router/lib/src/state.dart @@ -2,13 +2,10 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'package:collection/collection.dart'; -import 'package:flutter/foundation.dart'; import 'package:flutter/widgets.dart'; import '../go_router.dart'; import 'configuration.dart'; -import 'matching.dart'; import 'misc/errors.dart'; /// The route state during routing. @@ -255,208 +252,3 @@ class GoRouterStateRegistry extends ChangeNotifier { } } } - -/// The snapshot of the current state of a [StatefulShellRoute]. -/// -/// Note that this an immutable class, that represents the snapshot of the state -/// of a StatefulShellRoute at a given point in time. Therefore, instances of -/// this object should not be stored, but instead fetched fresh when needed, -/// using the method [StatefulShellRoute.of]. -@immutable -class StatefulShellRouteState { - /// Constructs a [StatefulShellRouteState]. - const StatefulShellRouteState({ - required this.route, - required this.branchStates, - required this.currentIndex, - required void Function( - StatefulShellBranchState, UnmodifiableRouteMatchList?) - switchActiveBranch, - required void Function() resetState, - }) : _switchActiveBranch = switchActiveBranch, - _resetState = resetState; - - /// Constructs a copy of this [StatefulShellRouteState], with updated values - /// for some of the fields. - StatefulShellRouteState copy( - {List? branchStates, int? currentIndex}) { - return StatefulShellRouteState( - route: route, - branchStates: branchStates ?? this.branchStates, - currentIndex: currentIndex ?? this.currentIndex, - switchActiveBranch: _switchActiveBranch, - resetState: _resetState, - ); - } - - /// The associated [StatefulShellRoute] - final StatefulShellRoute route; - - /// The state for all separate route branches associated with a - /// [StatefulShellRoute]. - final List branchStates; - - /// The state associated with the current [StatefulShellBranch]. - StatefulShellBranchState get currentBranchState => branchStates[currentIndex]; - - /// The index of the currently active [StatefulShellBranch]. - /// - /// Corresponds to the index of the branch in the List returned from - /// branchBuilder of [StatefulShellRoute]. - final int currentIndex; - - /// The Navigator key of the current navigator. - GlobalKey get currentNavigatorKey => - currentBranchState.branch.navigatorKey; - - final void Function(StatefulShellBranchState, UnmodifiableRouteMatchList?) - _switchActiveBranch; - - final void Function() _resetState; - - /// Gets the [Widget]s representing each of the shell branches. - /// - /// The Widget returned from this method contains the [Navigator]s of the - /// branches. Note that the Widgets returned by this method should only be - /// added to the widget tree if using a custom branch container Widget - /// implementation, where the child parameter in the [ShellRouteBuilder] of - /// the [StatefulShellRoute] is ignored (i.e. not added to the widget tree). - /// See [StatefulShellBranchState.child]. - List get children => - branchStates.map((StatefulShellBranchState e) => e.child).toList(); - - /// Navigate to the current location of the shell navigator with the provided - /// Navigator key, name or index. - /// - /// This method will switch the currently active [Navigator] for the - /// [StatefulShellRoute] by replacing the current navigation stack with the - /// one of the route branch identified by the provided Navigator key, name or - /// index. If resetLocation is true, the branch will be reset to its default - /// location (see [StatefulShellBranch.defaultLocation]). - void goBranch({ - GlobalKey? navigatorKey, - String? name, - int? index, - bool resetLocation = false, - }) { - assert(navigatorKey != null || name != null || index != null); - assert([navigatorKey, name, index].whereNotNull().length == 1); - - final StatefulShellBranchState? state; - if (navigatorKey != null) { - state = branchStates.firstWhereOrNull((StatefulShellBranchState e) => - e.branch.navigatorKey == navigatorKey); - if (state == null) { - throw GoError('Unable to find branch with key $navigatorKey'); - } - } else if (name != null) { - state = branchStates.firstWhereOrNull( - (StatefulShellBranchState e) => e.branch.name == name); - if (state == null) { - throw GoError('Unable to find branch with name "$name"'); - } - } else { - state = branchStates[index!]; - } - - _switchActiveBranch(state, resetLocation ? null : state._matchList); - } - - /// Refreshes this StatefulShellRouteState by rebuilding the state for the - /// current location. - void refresh() { - _switchActiveBranch(currentBranchState, currentBranchState._matchList); - } - - /// Resets this StatefulShellRouteState by clearing all navigation state of - /// the branches, and returning the current branch to its default location. - void reset() { - _resetState(); - } - - @override - bool operator ==(Object other) { - if (identical(other, this)) { - return true; - } - if (other is! StatefulShellRouteState) { - return false; - } - return other.route == route && - listEquals(other.branchStates, branchStates) && - other.currentIndex == currentIndex; - } - - @override - int get hashCode => Object.hash(route, currentIndex, currentIndex); -} - -/// The snapshot of the current state for a particular route branch -/// ([StatefulShellBranch]) in a [StatefulShellRoute]. -/// -/// Note that this an immutable class, that represents the snapshot of the state -/// of a StatefulShellBranchState at a given point in time. Therefore, instances of -/// this object should not be stored, but instead fetched fresh when needed, -/// via the [StatefulShellRouteState] returned by the method -/// [StatefulShellRoute.of]. -@immutable -class StatefulShellBranchState { - /// Constructs a [StatefulShellBranchState]. - const StatefulShellBranchState({ - required this.branch, - required this.child, - UnmodifiableRouteMatchList? matchList, - }) : _matchList = matchList; - - /// Constructs a copy of this [StatefulShellBranchState], with updated values for - /// some of the fields. - StatefulShellBranchState copy( - {Widget? child, UnmodifiableRouteMatchList? matchList}) { - return StatefulShellBranchState( - branch: branch, - child: child ?? this.child, - matchList: matchList ?? _matchList, - ); - } - - /// The associated [StatefulShellBranch] - final StatefulShellBranch branch; - - /// The [Widget] representing this route branch in a [StatefulShellRoute]. - /// - /// The Widget returned from this method contains the [Navigator] of the - /// branch. Note that the Widget returned by this method should only - /// be added to the widget tree if using a custom branch container Widget - /// implementation, where the child parameter in the [ShellRouteBuilder] of - /// the [StatefulShellRoute] is ignored (i.e. not added to the widget tree). - final Widget child; - - /// The current navigation stack for the branch. - final UnmodifiableRouteMatchList? _matchList; - - /// Returns true if this branch has been loaded (i.e. visited once or - /// pre-loaded). - bool get isLoaded => _matchList != null; - - @override - bool operator ==(Object other) { - if (identical(other, this)) { - return true; - } - if (other is! StatefulShellBranchState) { - return false; - } - return other.branch == branch && - other.child == child && - other._matchList == _matchList; - } - - @override - int get hashCode => Object.hash(branch, child, _matchList); -} - -/// Helper extension on [StatefulShellBranchState], for internal use. -extension StatefulShellBranchStateHelper on StatefulShellBranchState { - /// The current navigation stack for the branch. - UnmodifiableRouteMatchList? get matchList => _matchList; -} diff --git a/packages/go_router/test/builder_test.dart b/packages/go_router/test/builder_test.dart index ac405478321b..01b684cab630 100644 --- a/packages/go_router/test/builder_test.dart +++ b/packages/go_router/test/builder_test.dart @@ -103,15 +103,14 @@ void main() { StatefulShellRoute( builder: (_, __, Widget child) => child, branches: [ - StatefulShellBranch(rootLocation: '/nested', navigatorKey: key), - ], - routes: [ - GoRoute( - path: '/nested', - builder: (BuildContext context, GoRouterState state) { - return _DetailsScreen(); - }, - ), + StatefulShellBranch(navigatorKey: key, routes: [ + GoRoute( + path: '/nested', + builder: (BuildContext context, GoRouterState state) { + return _DetailsScreen(); + }, + ), + ]), ], ), ], @@ -156,16 +155,14 @@ void main() { shell = StatefulShellRoute( builder: (_, __, Widget child) => child, branches: [ - StatefulShellBranch( - rootLocation: '/root/nested', navigatorKey: key), - ], - routes: [ - nested = GoRoute( - path: 'nested', - builder: (BuildContext context, GoRouterState state) { - return _DetailsScreen(); - }, - ), + StatefulShellBranch(navigatorKey: key, routes: [ + nested = GoRoute( + path: 'nested', + builder: (BuildContext context, GoRouterState state) { + return _DetailsScreen(); + }, + ), + ]), ], ), ], @@ -197,147 +194,149 @@ void main() { expect(find.byKey(key), findsOneWidget); }); - testWidgets( - 'throws when a branch of a StatefulShellRoute has an incorrect ' - 'defaultLocation', (WidgetTester tester) async { - final RouteConfiguration config = RouteConfiguration( - routes: [ - StatefulShellRoute( - routes: [ - GoRoute( - path: '/a', - builder: (_, __) => _DetailsScreen(), - ), - GoRoute( - path: '/b', - builder: (_, __) => _DetailsScreen(), - ), - ], - builder: (_, __, Widget child) { - return _HomeScreen(child: child); - }, - branches: [ - StatefulShellBranch(rootLocation: '/x'), - StatefulShellBranch(rootLocation: '/b'), - ]), - ], - redirectLimit: 10, - topRedirect: (_, __) => null, - navigatorKey: GlobalKey(), - ); - - final RouteMatchList matches = RouteMatchList( - [ - _createRouteMatch(config.routes.first, '/b'), - _createRouteMatch(config.routes.first.routes.first, '/b'), - ], - Uri.parse('/b'), - const {}); - - await tester.pumpWidget( - _BuilderTestWidget( - routeConfiguration: config, - matches: matches, - ), - ); - - expect(tester.takeException(), isAssertionError); - }); - - testWidgets( - 'throws when a branch of a StatefulShellRoute has duplicate ' - 'defaultLocation', (WidgetTester tester) async { - final RouteConfiguration config = RouteConfiguration( - routes: [ - StatefulShellRoute( - routes: [ - GoRoute( - path: '/a', - builder: (_, __) => _DetailsScreen(), - ), - GoRoute( - path: '/b', - builder: (_, __) => _DetailsScreen(), - ), - ], - builder: (_, __, Widget child) { - return _HomeScreen(child: child); - }, - branches: [ - StatefulShellBranch(rootLocation: '/a'), - StatefulShellBranch(rootLocations: const ['/a', '/b']), - ]), - ], - redirectLimit: 10, - topRedirect: (_, __) => null, - navigatorKey: GlobalKey(), - ); - - final RouteMatchList matches = RouteMatchList( - [ - _createRouteMatch(config.routes.first, '/b'), - _createRouteMatch(config.routes.first.routes.first, '/b'), - ], - Uri.parse('/b'), - const {}); - - await tester.pumpWidget( - _BuilderTestWidget( - routeConfiguration: config, - matches: matches, - ), - ); - - expect(tester.takeException(), isAssertionError); - }); - - testWidgets('throws when StatefulShellRoute has duplicate navigator keys', - (WidgetTester tester) async { - final GlobalKey keyA = - GlobalKey(debugLabel: 'A'); - final RouteConfiguration config = RouteConfiguration( - routes: [ - StatefulShellRoute( - routes: [ - GoRoute( - path: '/a', - builder: (_, __) => _DetailsScreen(), - ), - GoRoute( - path: '/b', - builder: (_, __) => _DetailsScreen(), - ), - ], - builder: (_, __, Widget child) { - return _HomeScreen(child: child); - }, - branches: [ - StatefulShellBranch(rootLocation: '/a', navigatorKey: keyA), - StatefulShellBranch(rootLocation: '/b', navigatorKey: keyA), - ]), - ], - redirectLimit: 10, - topRedirect: (_, __) => null, - navigatorKey: GlobalKey(), - ); - - final RouteMatchList matches = RouteMatchList( - [ - _createRouteMatch(config.routes.first, '/b'), - _createRouteMatch(config.routes.first.routes.first, '/b'), - ], - Uri.parse('/b'), - const {}); - - await tester.pumpWidget( - _BuilderTestWidget( - routeConfiguration: config, - matches: matches, - ), - ); - - expect(tester.takeException(), isAssertionError); - }); + // testWidgets( + // 'throws when a branch of a StatefulShellRoute has an incorrect ' + // 'defaultLocation', (WidgetTester tester) async { + // final RouteConfiguration config = RouteConfiguration( + // routes: [ + // StatefulShellRoute( + // builder: (_, __, Widget child) { + // return _HomeScreen(child: child); + // }, + // branches: [ + // StatefulShellBranch(defaultLocation: '/x', routes: [ + // GoRoute( + // path: '/a', + // builder: (_, __) => _DetailsScreen(), + // ), + // ], + // ), + // StatefulShellBranch(defaultLocation: '/b', routes: [ + // GoRoute( + // path: '/b', + // builder: (_, __) => _DetailsScreen(), + // ), + // ], + // ), + // ]), + // ], + // redirectLimit: 10, + // topRedirect: (_, __) => null, + // navigatorKey: GlobalKey(), + // ); + // + // final RouteMatchList matches = RouteMatchList( + // [ + // _createRouteMatch(config.routes.first, '/b'), + // _createRouteMatch(config.routes.first.routes.first, '/b'), + // ], + // Uri.parse('/b'), + // const {}); + // + // await tester.pumpWidget( + // _BuilderTestWidget( + // routeConfiguration: config, + // matches: matches, + // ), + // ); + // + // expect(tester.takeException(), isAssertionError); + // }); + + // testWidgets( + // 'throws when a branch of a StatefulShellRoute has duplicate ' + // 'defaultLocation', (WidgetTester tester) async { + // final RouteConfiguration config = RouteConfiguration( + // routes: [ + // StatefulShellRoute( + // routes: [ + // GoRoute( + // path: '/a', + // builder: (_, __) => _DetailsScreen(), + // ), + // GoRoute( + // path: '/b', + // builder: (_, __) => _DetailsScreen(), + // ), + // ], + // builder: (_, __, Widget child) { + // return _HomeScreen(child: child); + // }, + // branches: [ + // StatefulShellBranch(rootLocation: '/a'), + // StatefulShellBranch(rootLocations: const ['/a', '/b']), + // ]), + // ], + // redirectLimit: 10, + // topRedirect: (_, __) => null, + // navigatorKey: GlobalKey(), + // ); + // + // final RouteMatchList matches = RouteMatchList( + // [ + // _createRouteMatch(config.routes.first, '/b'), + // _createRouteMatch(config.routes.first.routes.first, '/b'), + // ], + // Uri.parse('/b'), + // const {}); + // + // await tester.pumpWidget( + // _BuilderTestWidget( + // routeConfiguration: config, + // matches: matches, + // ), + // ); + // + // expect(tester.takeException(), isAssertionError); + // }); + + // testWidgets('throws when StatefulShellRoute has duplicate navigator keys', + // (WidgetTester tester) async { + // final GlobalKey keyA = + // GlobalKey(debugLabel: 'A'); + // final RouteConfiguration config = RouteConfiguration( + // routes: [ + // StatefulShellRoute( + // routes: [ + // GoRoute( + // path: '/a', + // builder: (_, __) => _DetailsScreen(), + // ), + // GoRoute( + // path: '/b', + // builder: (_, __) => _DetailsScreen(), + // ), + // ], + // builder: (_, __, Widget child) { + // return _HomeScreen(child: child); + // }, + // branches: [ + // StatefulShellBranch(rootLocation: '/a', navigatorKey: keyA), + // StatefulShellBranch(rootLocation: '/b', navigatorKey: keyA), + // ]), + // ], + // redirectLimit: 10, + // topRedirect: (_, __) => null, + // navigatorKey: GlobalKey(), + // ); + // + // final RouteMatchList matches = RouteMatchList( + // [ + // _createRouteMatch(config.routes.first, '/b'), + // _createRouteMatch(config.routes.first.routes.first, '/b'), + // ], + // Uri.parse('/b'), + // const {}); + // + // await tester.pumpWidget( + // _BuilderTestWidget( + // routeConfiguration: config, + // matches: matches, + // ), + // ); + // + // expect(tester.takeException(), isAssertionError); + // }); testWidgets('Uses the correct navigatorKey', (WidgetTester tester) async { final GlobalKey rootNavigatorKey = @@ -583,17 +582,16 @@ void main() { }, branches: [ StatefulShellBranch( - rootLocation: '/a', navigatorKey: shellNavigatorKey, restorationScopeId: 'scope1', - ), - ], - routes: [ - GoRoute( - path: '/a', - builder: (BuildContext context, GoRouterState state) { - return _DetailsScreen(); - }, + routes: [ + GoRoute( + path: '/a', + builder: (BuildContext context, GoRouterState state) { + return _DetailsScreen(); + }, + ), + ], ), ], ), diff --git a/packages/go_router/test/configuration_test.dart b/packages/go_router/test/configuration_test.dart index 36a6be39d00f..8b49ebfa2ea1 100644 --- a/packages/go_router/test/configuration_test.dart +++ b/packages/go_router/test/configuration_test.dart @@ -93,14 +93,57 @@ void main() { navigatorKey: root, routes: [ StatefulShellRoute(branches: [ - StatefulShellBranch(rootLocation: '/a'), - StatefulShellBranch(rootLocation: '/b'), - ], routes: [ - GoRoute( - path: '/a', - builder: _mockScreenBuilder, - parentNavigatorKey: someNavigatorKey), - GoRoute(path: '/b', builder: _mockScreenBuilder) + StatefulShellBranch( + routes: [ + GoRoute( + path: '/a', + builder: _mockScreenBuilder, + routes: [ + GoRoute( + path: 'details', + builder: _mockScreenBuilder, + parentNavigatorKey: someNavigatorKey), + ]), + ], + ), + StatefulShellBranch( + routes: [ + GoRoute( + path: '/b', + builder: _mockScreenBuilder, + parentNavigatorKey: someNavigatorKey), + ], + ), + ], builder: _mockShellBuilder), + ], + redirectLimit: 10, + topRedirect: (BuildContext context, GoRouterState state) { + return null; + }, + ); + }, + throwsAssertionError, + ); + }); + + test('throws when StatefulShellRoute has duplicate navigator keys', () { + final GlobalKey root = + GlobalKey(debugLabel: 'root'); + final GlobalKey keyA = + GlobalKey(debugLabel: 'A'); + final List shellRouteChildren = [ + GoRoute( + path: '/a', builder: _mockScreenBuilder, parentNavigatorKey: keyA), + GoRoute( + path: '/b', builder: _mockScreenBuilder, parentNavigatorKey: keyA), + ]; + expect( + () { + RouteConfiguration( + navigatorKey: root, + routes: [ + StatefulShellRoute(branches: [ + StatefulShellBranch(routes: shellRouteChildren) ], builder: _mockShellBuilder), ], redirectLimit: 10, @@ -114,75 +157,232 @@ void main() { }); test( - 'does not throw when a branch of a StatefulShellRoute has correctly ' - 'configured defaultLocations', () { + 'throws when a child of StatefulShellRoute has an incorrect ' + 'parentNavigatorKey', () { final GlobalKey root = GlobalKey(debugLabel: 'root'); + final GlobalKey sectionANavigatorKey = + GlobalKey(); + final GlobalKey sectionBNavigatorKey = + GlobalKey(); + final GoRoute routeA = GoRoute( + path: '/a', + builder: _mockScreenBuilder, + parentNavigatorKey: sectionBNavigatorKey); + final GoRoute routeB = GoRoute( + path: '/b', + builder: _mockScreenBuilder, + parentNavigatorKey: sectionANavigatorKey); + expect( + () { + RouteConfiguration( + navigatorKey: root, + routes: [ + StatefulShellRoute(branches: [ + StatefulShellBranch( + routes: [routeA], + navigatorKey: sectionANavigatorKey), + StatefulShellBranch( + routes: [routeB], + navigatorKey: sectionBNavigatorKey), + ], builder: _mockShellBuilder), + ], + redirectLimit: 10, + topRedirect: (BuildContext context, GoRouterState state) { + return null; + }, + ); + }, + throwsAssertionError, + ); + }); - RouteConfiguration( - navigatorKey: root, - routes: [ - StatefulShellRoute(branches: [ - StatefulShellBranch(rootLocation: '/a'), - StatefulShellBranch(rootLocation: '/b/detail'), - StatefulShellBranch(rootLocation: '/c/detail'), - StatefulShellBranch(rootLocation: '/e'), - ], routes: [ - GoRoute( - path: '/a', - builder: _mockScreenBuilder, - routes: [ - GoRoute( - path: 'detail', - builder: _mockScreenBuilder, - ), - ]), - GoRoute( - path: '/b', - builder: _mockScreenBuilder, - routes: [ - GoRoute( - path: 'detail', - builder: _mockScreenBuilder, - ), - ]), - StatefulShellRoute(branches: [ - StatefulShellBranch(rootLocation: '/c/detail'), - StatefulShellBranch(rootLocation: '/d/detail'), - ], routes: [ - GoRoute( - path: '/c', - builder: _mockScreenBuilder, + test( + 'throws when a branch of a StatefulShellRoute has an incorrect ' + 'defaultLocation', () { + final GlobalKey root = + GlobalKey(debugLabel: 'root'); + final GlobalKey sectionANavigatorKey = + GlobalKey(); + final GlobalKey sectionBNavigatorKey = + GlobalKey(); + expect( + () { + RouteConfiguration( + navigatorKey: root, + routes: [ + StatefulShellRoute(branches: [ + StatefulShellBranch( + defaultLocation: '/x', + navigatorKey: sectionANavigatorKey, routes: [ GoRoute( - path: 'detail', + path: '/a', builder: _mockScreenBuilder, ), - ]), - GoRoute( - path: '/d', - builder: _mockScreenBuilder, + ], + ), + StatefulShellBranch( + navigatorKey: sectionBNavigatorKey, routes: [ GoRoute( - path: 'detail', + path: '/b', builder: _mockScreenBuilder, ), - ]), - ], builder: _mockShellBuilder), - ShellRoute( - builder: _mockShellBuilder, - routes: [ - ShellRoute( - builder: _mockShellBuilder, + ], + ), + ], builder: _mockShellBuilder), + ], + redirectLimit: 10, + topRedirect: (BuildContext context, GoRouterState state) { + return null; + }, + ); + }, + throwsAssertionError, + ); + }); + + test( + 'throws when a branch of a StatefulShellRoute has a defaultLocation ' + 'that is not a descendant of the same branch', () { + final GlobalKey root = + GlobalKey(debugLabel: 'root'); + final GlobalKey sectionANavigatorKey = + GlobalKey(); + final GlobalKey sectionBNavigatorKey = + GlobalKey(); + expect( + () { + RouteConfiguration( + navigatorKey: root, + routes: [ + StatefulShellRoute(branches: [ + StatefulShellBranch( + defaultLocation: '/b', + navigatorKey: sectionANavigatorKey, routes: [ GoRoute( - path: '/e', + path: '/a', builder: _mockScreenBuilder, ), ], - ) + ), + StatefulShellBranch( + defaultLocation: '/b', + navigatorKey: sectionBNavigatorKey, + routes: [ + StatefulShellRoute(branches: [ + StatefulShellBranch( + routes: [ + GoRoute( + path: '/b', + builder: _mockScreenBuilder, + ), + ], + ), + ], builder: _mockShellBuilder), + ], + ), + ], builder: _mockShellBuilder), + ], + redirectLimit: 10, + topRedirect: (BuildContext context, GoRouterState state) { + return null; + }, + ); + }, + throwsAssertionError, + ); + }); + + test( + 'does not throw when a branch of a StatefulShellRoute has correctly ' + 'configured defaultLocations', () { + final GlobalKey root = + GlobalKey(debugLabel: 'root'); + + RouteConfiguration( + navigatorKey: root, + routes: [ + StatefulShellRoute(branches: [ + StatefulShellBranch( + routes: [ + GoRoute( + path: '/a', + builder: _mockScreenBuilder, + routes: [ + GoRoute( + path: 'detail', + builder: _mockScreenBuilder, + ), + ]), + ], + ), + StatefulShellBranch( + defaultLocation: '/b/detail', + routes: [ + GoRoute( + path: '/b', + builder: _mockScreenBuilder, + routes: [ + GoRoute( + path: 'detail', + builder: _mockScreenBuilder, + ), + ]), + ], + ), + StatefulShellBranch( + defaultLocation: '/c/detail', + routes: [ + StatefulShellRoute(branches: [ + StatefulShellBranch( + routes: [ + GoRoute( + path: '/c', + builder: _mockScreenBuilder, + routes: [ + GoRoute( + path: 'detail', + builder: _mockScreenBuilder, + ), + ]), + ], + ), + StatefulShellBranch( + defaultLocation: '/d/detail', + routes: [ + GoRoute( + path: '/d', + builder: _mockScreenBuilder, + routes: [ + GoRoute( + path: 'detail', + builder: _mockScreenBuilder, + ), + ]), + ], + ), + ], builder: _mockShellBuilder), ], ), + StatefulShellBranch(routes: [ + ShellRoute( + builder: _mockShellBuilder, + routes: [ + ShellRoute( + builder: _mockShellBuilder, + routes: [ + GoRoute( + path: '/e', + builder: _mockScreenBuilder, + ), + ], + ) + ], + ), + ]), ], builder: _mockShellBuilder), ], redirectLimit: 10, @@ -192,6 +392,87 @@ void main() { ); }); + test( + 'derives the correct defaultLocation for a ShellRouteBranch', + () { + final StatefulShellBranch branchA; + final StatefulShellBranch branchY; + final StatefulShellBranch branchB; + + final RouteConfiguration config = RouteConfiguration( + navigatorKey: GlobalKey(debugLabel: 'root'), + routes: [ + StatefulShellRoute( + builder: (_, __, Widget child) => child, + branches: [ + branchA = StatefulShellBranch(routes: [ + GoRoute( + path: '/a', + builder: _mockScreenBuilder, + routes: [ + GoRoute( + path: 'x', + builder: _mockScreenBuilder, + routes: [ + StatefulShellRoute( + builder: (_, __, Widget child) => child, + branches: [ + branchY = + StatefulShellBranch(routes: [ + ShellRoute( + builder: _mockShellBuilder, + routes: [ + GoRoute( + path: 'y1', + builder: _mockScreenBuilder, + ), + GoRoute( + path: 'y2', + builder: _mockScreenBuilder, + ), + ]) + ]) + ]), + ], + ), + ], + ), + ]), + branchB = StatefulShellBranch(routes: [ + ShellRoute( + builder: _mockShellBuilder, + routes: [ + ShellRoute( + builder: _mockShellBuilder, + routes: [ + GoRoute( + path: '/b1', + builder: _mockScreenBuilder, + ), + GoRoute( + path: '/b2', + builder: _mockScreenBuilder, + ), + ], + ) + ], + ), + ]), + ], + ), + ], + redirectLimit: 10, + topRedirect: (BuildContext context, GoRouterState state) { + return null; + }, + ); + + expect('/a', config.findShellRouteBranchDefaultLocation(branchA)); + expect('/a/x/y1', config.findShellRouteBranchDefaultLocation(branchY)); + expect('/b1', config.findShellRouteBranchDefaultLocation(branchB)); + }, + ); + test( 'throws when there is a GoRoute ancestor with a different parentNavigatorKey', () { diff --git a/packages/go_router/test/delegate_test.dart b/packages/go_router/test/delegate_test.dart index 2bb922a3d824..5c608c920091 100644 --- a/packages/go_router/test/delegate_test.dart +++ b/packages/go_router/test/delegate_test.dart @@ -38,13 +38,8 @@ Future createGoRouterWithStatefulShellRoute( routes: [ GoRoute(path: '/', builder: (_, __) => const DummyStatefulWidget()), GoRoute(path: '/a', builder: (_, __) => const DummyStatefulWidget()), - StatefulShellRoute( - branches: [ - StatefulShellBranch(rootLocation: '/c'), - StatefulShellBranch(rootLocation: '/d'), - ], - builder: (_, __, Widget child) => child, - routes: [ + StatefulShellRoute(branches: [ + StatefulShellBranch(routes: [ GoRoute( path: '/c', builder: (_, __) => const DummyStatefulWidget(), @@ -56,6 +51,8 @@ Future createGoRouterWithStatefulShellRoute( path: 'c2', builder: (_, __) => const DummyStatefulWidget()), ]), + ]), + StatefulShellBranch(routes: [ GoRoute( path: '/d', builder: (_, __) => const DummyStatefulWidget(), @@ -64,8 +61,8 @@ Future createGoRouterWithStatefulShellRoute( path: 'd1', builder: (_, __) => const DummyStatefulWidget()), ]), - ], - ), + ]), + ], builder: (_, __, Widget child) => child), ], ); await tester.pumpWidget(MaterialApp.router( diff --git a/packages/go_router/test/go_router_test.dart b/packages/go_router/test/go_router_test.dart index 1217b119f54d..98fdc756a83b 100644 --- a/packages/go_router/test/go_router_test.dart +++ b/packages/go_router/test/go_router_test.dart @@ -2295,41 +2295,46 @@ void main() { final List routes = [ StatefulShellRoute( builder: (BuildContext context, _, Widget child) { - routeState = StatefulShellRoute.of(context); + routeState = StatefulShellRouteState.of(context); return child; }, - routes: [ - GoRoute( - path: '/a', - builder: (BuildContext context, GoRouterState state) => - const Text('Screen A'), + branches: [ + StatefulShellBranch( + routes: [ + GoRoute( + path: '/a', + builder: (BuildContext context, GoRouterState state) => + const Text('Screen A'), + ), + ], ), - GoRoute( - path: '/family', - builder: (BuildContext context, GoRouterState state) => - const Text('Families'), - routes: [ - GoRoute( - path: ':fid', + StatefulShellBranch( + routes: [ + GoRoute( + path: '/family', builder: (BuildContext context, GoRouterState state) => - FamilyScreen(state.params['fid']!), - routes: [ + const Text('Families'), + routes: [ GoRoute( - path: 'person/:pid', - builder: (BuildContext context, GoRouterState state) { - final String fid = state.params['fid']!; - final String pid = state.params['pid']!; + path: ':fid', + builder: (BuildContext context, GoRouterState state) => + FamilyScreen(state.params['fid']!), + routes: [ + GoRoute( + path: 'person/:pid', + builder: + (BuildContext context, GoRouterState state) { + final String fid = state.params['fid']!; + final String pid = state.params['pid']!; - return PersonScreen(fid, pid); - }, - ), - ], - ) - ]), - ], - branches: [ - StatefulShellBranch(rootLocation: '/a'), - StatefulShellBranch(rootLocation: '/family'), + return PersonScreen(fid, pid); + }, + ), + ], + ) + ]), + ], + ), ], ), ]; @@ -2811,37 +2816,48 @@ void main() { final List routes = [ StatefulShellRoute( builder: (BuildContext context, GoRouterState state, Widget child) { - routeState = StatefulShellRoute.of(context); + routeState = StatefulShellRouteState.of(context); return child; }, - routes: [ - GoRoute( - path: '/a', - builder: (BuildContext context, GoRouterState state) => - const Text('Screen A'), - ), - GoRoute( - path: '/b', - builder: (BuildContext context, GoRouterState state) => - const Text('Screen B'), + branches: [ + StatefulShellBranch( + routes: [ + GoRoute( + path: '/a', + builder: (BuildContext context, GoRouterState state) => + const Text('Screen A'), + ), + ], ), - GoRoute( - path: '/c', - builder: (BuildContext context, GoRouterState state) => - const Text('Screen C'), + StatefulShellBranch( + name: 'B', + routes: [ + GoRoute( + path: '/b', + builder: (BuildContext context, GoRouterState state) => + const Text('Screen B'), + ), + ], ), - GoRoute( - path: '/d', - builder: (BuildContext context, GoRouterState state) => - const Text('Screen D'), + StatefulShellBranch( + navigatorKey: branchCNavigatorKey, + routes: [ + GoRoute( + path: '/c', + builder: (BuildContext context, GoRouterState state) => + const Text('Screen C'), + ), + ], ), - ], - branches: [ - StatefulShellBranch(rootLocation: '/a'), - StatefulShellBranch(rootLocation: '/b', name: 'B'), StatefulShellBranch( - rootLocation: '/c', navigatorKey: branchCNavigatorKey), - StatefulShellBranch(rootLocation: '/d'), + routes: [ + GoRoute( + path: '/d', + builder: (BuildContext context, GoRouterState state) => + const Text('Screen D'), + ), + ], + ), ], ), ]; @@ -2908,34 +2924,34 @@ void main() { final List routes = [ StatefulShellRoute( builder: (BuildContext context, GoRouterState state, Widget child) { - routeState = StatefulShellRoute.of(context); + routeState = StatefulShellRouteState.of(context); return child; }, - routes: [ - GoRoute( - path: '/a', - builder: (BuildContext context, GoRouterState state) => - const Text('Screen A'), - routes: [ - GoRoute( - path: 'detailA', - builder: (BuildContext context, GoRouterState state) => - Column(children: [ - const Text('Screen A Detail'), - DummyStatefulWidget(key: statefulWidgetKey), - ]), - ), - ], - ), - GoRoute( - path: '/b', - builder: (BuildContext context, GoRouterState state) => - const Text('Screen B'), - ), - ], branches: [ - StatefulShellBranch(rootLocation: '/a'), - StatefulShellBranch(rootLocation: '/b'), + StatefulShellBranch(routes: [ + GoRoute( + path: '/a', + builder: (BuildContext context, GoRouterState state) => + const Text('Screen A'), + routes: [ + GoRoute( + path: 'detailA', + builder: (BuildContext context, GoRouterState state) => + Column(children: [ + const Text('Screen A Detail'), + DummyStatefulWidget(key: statefulWidgetKey), + ]), + ), + ], + ), + ]), + StatefulShellBranch(routes: [ + GoRoute( + path: '/b', + builder: (BuildContext context, GoRouterState state) => + const Text('Screen B'), + ), + ]), ], ), ]; @@ -2980,40 +2996,42 @@ void main() { final List routes = [ StatefulShellRoute( builder: (BuildContext context, GoRouterState state, Widget child) { - routeState = StatefulShellRoute.of(context); + routeState = StatefulShellRouteState.of(context); return child; }, - routes: [ - GoRoute( - path: '/a', - builder: (BuildContext context, GoRouterState state) => - const Text('Screen A'), - routes: [ - GoRoute( - path: 'detailA', - builder: (BuildContext context, GoRouterState state) => - const Text('Screen A Detail'), - ), - ], - ), - GoRoute( - path: '/b', - builder: (BuildContext context, GoRouterState state) => - const Text('Screen B'), - routes: [ - GoRoute( - path: 'detailB', - builder: (BuildContext context, GoRouterState state) => - const Text('Screen B Detail'), - ), - ], - ), - ], branches: [ StatefulShellBranch( - rootLocation: '/a', navigatorKey: sectionANavigatorKey), + navigatorKey: sectionANavigatorKey, + routes: [ + GoRoute( + path: '/a', + builder: (BuildContext context, GoRouterState state) => + const Text('Screen A'), + routes: [ + GoRoute( + path: 'detailA', + builder: (BuildContext context, GoRouterState state) => + const Text('Screen A Detail'), + ), + ], + ), + ]), StatefulShellBranch( - rootLocation: '/b', navigatorKey: sectionBNavigatorKey), + navigatorKey: sectionBNavigatorKey, + routes: [ + GoRoute( + path: '/b', + builder: (BuildContext context, GoRouterState state) => + const Text('Screen B'), + routes: [ + GoRoute( + path: 'detailB', + builder: (BuildContext context, GoRouterState state) => + const Text('Screen B Detail'), + ), + ], + ), + ]), ], ), ]; @@ -3062,24 +3080,24 @@ void main() { final List routes = [ StatefulShellRoute( builder: (BuildContext context, _, Widget child) { - routeState = StatefulShellRoute.of(context); + routeState = StatefulShellRouteState.of(context); return child; }, - routes: [ - GoRoute( - path: '/a', - builder: (BuildContext context, GoRouterState state) => - const Text('Screen A'), - ), - GoRoute( - path: '/b', - builder: (BuildContext context, GoRouterState state) => - Text('Screen B - ${state.extra}'), - ), - ], branches: [ - StatefulShellBranch(rootLocation: '/a'), - StatefulShellBranch(rootLocation: '/b'), + StatefulShellBranch(routes: [ + GoRoute( + path: '/a', + builder: (BuildContext context, GoRouterState state) => + const Text('Screen A'), + ), + ]), + StatefulShellBranch(routes: [ + GoRoute( + path: '/b', + builder: (BuildContext context, GoRouterState state) => + Text('Screen B - ${state.extra}'), + ), + ]), ], ), ]; @@ -3120,24 +3138,24 @@ void main() { ), StatefulShellRoute( builder: (BuildContext context, GoRouterState state, Widget child) { - routeState = StatefulShellRoute.of(context); + routeState = StatefulShellRouteState.of(context); return child; }, - routes: [ - GoRoute( - path: '/a', - builder: (BuildContext context, GoRouterState state) => - const Text('Screen A'), - ), - GoRoute( - path: '/b', - builder: (BuildContext context, GoRouterState state) => - Text('Screen B - ${state.extra ?? ''}'), - ), - ], branches: [ - StatefulShellBranch(rootLocation: '/a'), - StatefulShellBranch(rootLocation: '/b'), + StatefulShellBranch(routes: [ + GoRoute( + path: '/a', + builder: (BuildContext context, GoRouterState state) => + const Text('Screen A'), + ), + ]), + StatefulShellBranch(routes: [ + GoRoute( + path: '/b', + builder: (BuildContext context, GoRouterState state) => + const Text('Screen B'), + ), + ]), ], ), ]; @@ -3149,7 +3167,7 @@ void main() { router.go('/b'); await tester.pumpAndSettle(); expect(find.text('Screen A'), findsNothing); - expect(find.text('Screen B - '), findsOneWidget); + expect(find.text('Screen B'), findsOneWidget); router.push('/common', extra: 'X'); await tester.pumpAndSettle(); @@ -3187,53 +3205,63 @@ void main() { StatefulShellRoute( builder: (BuildContext context, GoRouterState state, Widget child) => child, - routes: [ - GoRoute( - path: '/a', - builder: (BuildContext context, GoRouterState state) => - DummyStatefulWidget(key: statefulWidgetKeyA), - ), - GoRoute( - path: '/b', - builder: (BuildContext context, GoRouterState state) => - DummyStatefulWidget(key: statefulWidgetKeyB), - ), - ], branches: [ - StatefulShellBranch(rootLocation: '/a'), - StatefulShellBranch(rootLocation: '/b'), + 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( builder: (BuildContext context, GoRouterState state, Widget child) => Scaffold(body: child), - routes: [ - GoRoute( - path: '/c', - builder: (BuildContext context, GoRouterState state) => - DummyStatefulWidget(key: statefulWidgetKeyC), + branches: [ + StatefulShellBranch( + preload: true, + routes: [ + GoRoute( + path: '/c', + builder: (BuildContext context, GoRouterState state) => + DummyStatefulWidget(key: statefulWidgetKeyC), + ), + ], ), - GoRoute( - path: '/d', - builder: (BuildContext context, GoRouterState state) => - DummyStatefulWidget(key: statefulWidgetKeyD), + StatefulShellBranch( + preload: true, + routes: [ + GoRoute( + path: '/d', + builder: (BuildContext context, GoRouterState state) => + DummyStatefulWidget(key: statefulWidgetKeyD), + ), + ], ), - GoRoute( - path: '/e', - builder: (BuildContext context, GoRouterState state) => - const Text('E'), - routes: [ - GoRoute( - path: 'details', + StatefulShellBranch( + preload: true, + routes: [ + GoRoute( + path: '/e', builder: (BuildContext context, GoRouterState state) => - DummyStatefulWidget(key: statefulWidgetKeyE), - ), - ]), - ], - branches: [ - StatefulShellBranch(rootLocation: '/c', preload: true), - StatefulShellBranch(rootLocation: '/d', preload: true), - StatefulShellBranch(rootLocation: '/e', preload: true), + const Text('E'), + routes: [ + GoRoute( + path: 'details', + builder: (BuildContext context, GoRouterState state) => + DummyStatefulWidget(key: statefulWidgetKeyE), + ), + ]), + ], + ), ], ), ]; @@ -3272,51 +3300,52 @@ void main() { final List routes = [ StatefulShellRoute( builder: (BuildContext context, GoRouterState state, Widget child) { - routeState = StatefulShellRoute.of(context); + routeState = StatefulShellRouteState.of(context); return child; }, - routes: [ - GoRoute( - path: '/a', - builder: (BuildContext context, GoRouterState state) => - const Text('Screen A'), - ), - GoRoute( - path: '/b', - builder: (BuildContext context, GoRouterState state) => - const Text('Screen B'), - routes: [ - GoRoute( - path: 'details1', - builder: (BuildContext context, GoRouterState state) => - const Text('Screen B Detail1'), - ), - GoRoute( - path: 'details2', - builder: (BuildContext context, GoRouterState state) => - const Text('Screen B Detail2'), - ), - ], - ), - GoRoute( - path: '/c', - redirect: (_, __) => '/c/main2', - ), - GoRoute( - path: '/c/main1', - builder: (BuildContext context, GoRouterState state) => - const Text('Screen C1'), - ), - GoRoute( - path: '/c/main2', - builder: (BuildContext context, GoRouterState state) => - const Text('Screen C2'), - ), - ], branches: [ - StatefulShellBranch(rootLocation: '/a'), - StatefulShellBranch(rootLocation: '/b'), - StatefulShellBranch(rootLocation: '/c'), + StatefulShellBranch(routes: [ + GoRoute( + path: '/a', + builder: (BuildContext context, GoRouterState state) => + const Text('Screen A'), + ), + ]), + StatefulShellBranch(routes: [ + GoRoute( + path: '/b', + builder: (BuildContext context, GoRouterState state) => + const Text('Screen B'), + routes: [ + GoRoute( + path: 'details1', + builder: (BuildContext context, GoRouterState state) => + const Text('Screen B Detail1'), + ), + GoRoute( + path: 'details2', + builder: (BuildContext context, GoRouterState state) => + const Text('Screen B Detail2'), + ), + ], + ), + ]), + StatefulShellBranch(routes: [ + GoRoute( + path: '/c', + redirect: (_, __) => '/c/main2', + ), + GoRoute( + path: '/c/main1', + builder: (BuildContext context, GoRouterState state) => + const Text('Screen C1'), + ), + GoRoute( + path: '/c/main2', + builder: (BuildContext context, GoRouterState state) => + const Text('Screen C2'), + ), + ]), ], ), ]; @@ -3365,38 +3394,38 @@ void main() { final List routes = [ StatefulShellRoute( builder: (BuildContext context, GoRouterState state, Widget child) { - routeState = StatefulShellRoute.of(context); + routeState = StatefulShellRouteState.of(context); return child; }, - routes: [ - GoRoute( - path: '/a', - builder: (BuildContext context, GoRouterState state) => - const Text('Screen A'), - routes: [ - GoRoute( - path: 'detail', - builder: (BuildContext context, GoRouterState state) => - const Text('Screen A Detail'), - ), - ], - ), - GoRoute( - path: '/b', - builder: (BuildContext context, GoRouterState state) => - const Text('Screen B'), - routes: [ - GoRoute( - path: 'detail', - builder: (BuildContext context, GoRouterState state) => - const Text('Screen B Detail'), - ), - ], - ), - ], branches: [ - StatefulShellBranch(rootLocation: '/a'), - StatefulShellBranch(rootLocation: '/b'), + StatefulShellBranch(routes: [ + GoRoute( + path: '/a', + builder: (BuildContext context, GoRouterState state) => + const Text('Screen A'), + routes: [ + GoRoute( + path: 'detail', + builder: (BuildContext context, GoRouterState state) => + const Text('Screen A Detail'), + ), + ], + ), + ]), + StatefulShellBranch(routes: [ + GoRoute( + path: '/b', + builder: (BuildContext context, GoRouterState state) => + const Text('Screen B'), + routes: [ + GoRoute( + path: 'detail', + builder: (BuildContext context, GoRouterState state) => + const Text('Screen B Detail'), + ), + ], + ), + ]), ], ), ]; @@ -3421,138 +3450,6 @@ void main() { expect(find.text('Screen A'), findsOneWidget); expect(find.text('Screen A Detail'), findsNothing); }); - - testWidgets( - 'Dynamic branches are created, removed and updated correctly in ' - 'a StatefulShellRoute', (WidgetTester tester) async { - final GlobalKey rootNavigatorKey = - GlobalKey(debugLabel: 'root'); - final GlobalKey branch0 = - GlobalKey(debugLabel: 'branch0'); - final GlobalKey branch1 = - GlobalKey(debugLabel: 'branch1'); - final GlobalKey branch2 = - GlobalKey(debugLabel: 'branch2'); - - StatefulShellRouteState? routeState; - final ValueNotifier scenario = ValueNotifier(1); - - final GoRouter router = GoRouter( - navigatorKey: rootNavigatorKey, - initialLocation: '/a/0', - routes: [ - StatefulShellRoute( - builder: (BuildContext context, GoRouterState state, Widget child) { - routeState = StatefulShellRoute.of(context); - return child; - }, - branchBuilder: (_, __) => [ - StatefulShellBranch(rootLocation: '/a/0', navigatorKey: branch0), - if (scenario.value == 2) ...[ - StatefulShellBranch( - rootLocation: '/a/1', navigatorKey: branch1), - StatefulShellBranch( - rootLocation: '/a/2', navigatorKey: branch2), - ], - if (scenario.value == 3) ...[ - StatefulShellBranch( - rootLocation: '/a/1', - name: 'branch1', - navigatorKey: branch1), - StatefulShellBranch( - rootLocation: '/a/2', navigatorKey: branch2), - ], - if (scenario.value == 4) - StatefulShellBranch( - rootLocation: '/a/1', navigatorKey: branch1), - ], - routes: [ - GoRoute( - path: '/a/:id', - builder: (BuildContext context, GoRouterState state) { - return Text('a-${state.params['id']}'); - }, - routes: [ - GoRoute( - path: 'detail', - builder: (BuildContext context, GoRouterState state) { - return Text('a-detail-${state.params['id']}'); - }, - ), - ]), - ], - ), - ], - errorBuilder: (BuildContext context, GoRouterState state) => - Text('error:${GoRouter.of(context).location}'), - ); - - await tester.pumpWidget( - ValueListenableBuilder( - valueListenable: scenario, - builder: (_, __, ___) { - return MaterialApp.router(routerConfig: router); - }), - ); - - expect(find.text('a-0'), findsOneWidget); - expect(find.text('a-detail-0'), findsNothing); - expect(find.byKey(branch0), findsOneWidget); - - router.go('/a/0/detail'); - await tester.pumpAndSettle(); - expect(find.text('a-0'), findsNothing); - expect(find.text('a-detail-0'), findsOneWidget); - - router.go('/a/1'); - await tester.pumpAndSettle(); - expect(find.text('a-1'), findsNothing); - expect(find.text('error:/a/1'), findsOneWidget); - - scenario.value = 2; - await tester.pumpAndSettle(); - routeState!.goBranch(navigatorKey: branch2); - await tester.pumpAndSettle(); - expect(find.text('a-0'), findsNothing); - expect(find.text('a-1'), findsNothing); - expect(find.text('a-2'), findsOneWidget); - - expect(() { - // Name 'branch1' hasn't yet been assigned, so this should fail - routeState!.goBranch(name: 'branch1'); - }, throwsA(isA())); - scenario.value = 3; - await tester.pumpAndSettle(); - routeState!.goBranch(name: 'branch1'); - await tester.pumpAndSettle(); - expect(find.text('a-0'), findsNothing); - expect(find.text('a-1'), findsOneWidget); - expect(find.text('a-2'), findsNothing); - expect(routeState!.branchStates.length, 3); - - router.go('/a/2/detail'); - await tester.pumpAndSettle(); - expect(find.text('a-2'), findsNothing); - expect(find.text('a-detail-2'), findsOneWidget); - routeState!.goBranch(name: 'branch1'); - await tester.pumpAndSettle(); - - // Test removal of branch2 - scenario.value = 4; - await tester.pumpAndSettle(); - expect(routeState!.branchStates.length, 2); - expect(() { - routeState!.goBranch(navigatorKey: branch2); - }, throwsA(isA())); - - // Test that state of branch2 is forgotten (not restored) after removal - scenario.value = 3; - await tester.pumpAndSettle(); - routeState!.goBranch(navigatorKey: branch2); - await tester.pumpAndSettle(); - expect(find.text('a-2'), findsOneWidget); - expect(find.text('a-detail-2'), findsNothing); - }); }); group('Imperative navigation', () { @@ -3689,37 +3586,37 @@ void main() { body: child, ); }, - routes: [ - GoRoute( - path: '/a', - builder: (BuildContext context, _) { - return const Scaffold( - body: Text('Screen A'), - ); - }, - ), - GoRoute( - path: '/b', - builder: (BuildContext context, _) { - return const Scaffold( - body: Text('Screen B'), - ); - }, - routes: [ - GoRoute( - path: 'detail', - builder: (BuildContext context, _) { - return const Scaffold( - body: Text('Screen B detail'), - ); - }, - ), - ], - ), - ], branches: [ - StatefulShellBranch(rootLocation: '/a'), - StatefulShellBranch(rootLocation: '/b'), + StatefulShellBranch(routes: [ + GoRoute( + path: '/a', + builder: (BuildContext context, _) { + return const Scaffold( + body: Text('Screen A'), + ); + }, + ), + ]), + StatefulShellBranch(routes: [ + GoRoute( + path: '/b', + builder: (BuildContext context, _) { + return const Scaffold( + body: Text('Screen B'), + ); + }, + routes: [ + GoRoute( + path: 'detail', + builder: (BuildContext context, _) { + return const Scaffold( + body: Text('Screen B detail'), + ); + }, + ), + ], + ), + ]), ], ), ], @@ -3745,6 +3642,7 @@ void main() { expect(rootNavigatorKey.currentState?.canPop(), false); }, ); + testWidgets('Pageless route should include in can pop', (WidgetTester tester) async { final GlobalKey root = From e6a4f7152098ff0afc06b25d0c4f6f79439bfd1a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20Lo=CC=88fstrand?= Date: Thu, 5 Jan 2023 03:14:59 +0100 Subject: [PATCH 072/112] Documentation updates and some renaming. Minor cleanup/refactoring in RouteBuilder/StatefulNavigationShell. --- .../example/lib/stateful_shell_route.dart | 30 ++++++------ packages/go_router/lib/src/builder.dart | 46 ++++++++++-------- packages/go_router/lib/src/configuration.dart | 18 +++---- .../src/misc/stateful_navigation_shell.dart | 6 ++- packages/go_router/lib/src/route.dart | 48 +++++++++++-------- .../go_router/test/configuration_test.dart | 9 ++-- 6 files changed, 88 insertions(+), 69 deletions(-) diff --git a/packages/go_router/example/lib/stateful_shell_route.dart b/packages/go_router/example/lib/stateful_shell_route.dart index e862ec5f5b98..a838b951d75a 100644 --- a/packages/go_router/example/lib/stateful_shell_route.dart +++ b/packages/go_router/example/lib/stateful_shell_route.dart @@ -32,12 +32,12 @@ class NestedTabNavigationExampleApp extends StatelessWidget { initialLocation: '/a', routes: [ StatefulShellRoute( - /// To enable preloading of the root routes of the branches, pass true - /// for the parameter preloadBranches. - // preloadBranches: true, branches: [ /// The route branch for the first tab of the bottom navigation bar. StatefulShellBranch( + /// To enable preloading of the default locations of branches, pass + /// true for the parameter preload. + // preload: true, navigatorKey: _tabANavigatorKey, routes: [ GoRoute( @@ -93,10 +93,10 @@ class NestedTabNavigationExampleApp extends StatelessWidget { /// The route branch for the third tab of the bottom navigation bar. StatefulShellBranch( - /// ShellRouteBranch will automatically use the first descendant + /// StatefulShellBranch will automatically use the first descendant /// GoRoute as the default location of the branch. If another route - /// is desired, you can specify the location of it using the - /// defaultLocation parameter. + /// is desired, specify the location of it using the defaultLocation + /// parameter. // defaultLocation: '/c2', routes: [ StatefulShellRoute( @@ -162,20 +162,22 @@ class NestedTabNavigationExampleApp extends StatelessWidget { return ScaffoldWithNavBar(body: child); }, - /// If you need to create a custom container for the branch routes, to - /// for instance setup custom animations, you can implement your builder - /// something like below (see _AnimatedRouteBranchContainer). Note that - /// in this case, you should not add the Widget provided in the child - /// parameter to the widget tree. Instead, you should use the child - /// widgets of each branch (see StatefulShellRouteState.children). + /// It's possible to customize the container for the branch navigators + /// even further, to for instance setup custom animations. The code + /// below is an example of such a customization (see + /// _AnimatedRouteBranchContainer). Note that in this case, the Widget + /// provided in the child parameter should not be added to the widget + /// tree. Instead, access the child widgets of each branch directly + /// (see StatefulShellRouteState.children) to implement a custom layout + /// and container for the navigators. // builder: (BuildContext context, GoRouterState state, Widget child) { // return ScaffoldWithNavBar( // body: _AnimatedRouteBranchContainer(), // ); // }, - /// If you need to customize the Page for StatefulShellRoute, pass a - /// pageBuilder function in addition to the builder, for example: + /// If it's necessary to customize the Page for StatefulShellRoute, + /// provide a pageBuilder function in addition to the builder, for example: // pageBuilder: // (BuildContext context, GoRouterState state, Widget statefulShell) { // return NoTransitionPage(child: statefulShell); diff --git a/packages/go_router/lib/src/builder.dart b/packages/go_router/lib/src/builder.dart index 36181a3492c4..235d92b5e25f 100644 --- a/packages/go_router/lib/src/builder.dart +++ b/packages/go_router/lib/src/builder.dart @@ -199,13 +199,12 @@ class RouteBuilder { Navigator buildNavigator(String? restorationScopeId) => _buildNavigator( onPopPage, keyToPages[shellNavigatorKey]!, shellNavigatorKey, restorationScopeId: restorationScopeId); - Widget child; + final Widget child; if (route is StatefulShellRoute) { final String? restorationScopeId = route.branchForSubRoute(subRoute).restorationScopeId; - child = buildNavigator(restorationScopeId); child = _buildStatefulNavigationShell( - route, child as Navigator, state, matchList, onPopPage, registry); + route, buildNavigator(restorationScopeId), state, matchList); } else { final String? restorationScopeId = (route is ShellRoute) ? route.restorationScopeId : null; @@ -244,25 +243,32 @@ class RouteBuilder { Navigator navigator, GoRouterState shellRouterState, RouteMatchList currentMatchList, - PopPageCallback pop, - Map, GoRouterState> registry, ) { return StatefulNavigationShell( - configuration: configuration, - shellRoute: shellRoute, - shellGoRouterState: shellRouterState, - currentNavigator: navigator, - currentMatchList: currentMatchList.unmodifiableRouteMatchList(), - branchPreloadNavigatorBuilder: (BuildContext context, - RouteMatchList matchList, - int startIndex, - GlobalKey navigatorKey, - String? restorationScopeId) { - final List> pages = buildPages(context, matchList, - startIndex, pop, true, navigatorKey, registry); - return _buildNavigator(pop, pages, navigatorKey, - restorationScopeId: restorationScopeId); - }); + configuration: configuration, + shellRoute: shellRoute, + shellGoRouterState: shellRouterState, + currentNavigator: navigator, + currentMatchList: currentMatchList.unmodifiableRouteMatchList(), + branchPreloadNavigatorBuilder: _preloadShellBranchNavigator, + ); + } + + Navigator _preloadShellBranchNavigator( + BuildContext context, + RouteMatchList matchList, + int startIndex, + GlobalKey navigatorKey, + PopPageCallback onPopPage, + String? restorationScopeId, + ) { + return _buildNavigator( + onPopPage, + buildPages(context, matchList, startIndex, onPopPage, true, navigatorKey, + , GoRouterState>{}), + navigatorKey, + restorationScopeId: restorationScopeId, + ); } /// Helper method that builds a [GoRouterState] object for the given [match] diff --git a/packages/go_router/lib/src/configuration.dart b/packages/go_router/lib/src/configuration.dart index bbf9be341156..01db86f76622 100644 --- a/packages/go_router/lib/src/configuration.dart +++ b/packages/go_router/lib/src/configuration.dart @@ -28,7 +28,7 @@ class RouteConfiguration { _debugVerifyNoDuplicatePathParameter(routes, {})), assert(_debugCheckParentNavigatorKeys( routes, >[navigatorKey])) { - assert(_debugCheckShellRouteBranchDefaultLocations( + assert(_debugCheckStatefulShellBranchDefaultLocations( routes, RouteMatcher(this))); _cacheNameToPath('', routes); log.info(_debugKnownRoutes()); @@ -126,7 +126,7 @@ class RouteConfiguration { // Check to see that the configured defaultLocation of StatefulShellBranches // points to a descendant route of the route branch. - bool _debugCheckShellRouteBranchDefaultLocations( + bool _debugCheckStatefulShellBranchDefaultLocations( List routes, RouteMatcher matcher) { try { for (final RouteBase route in routes) { @@ -135,7 +135,7 @@ class RouteConfiguration { if (branch.defaultLocation == null) { // Recursively search for the first GoRoute descendant. Will // throw assertion error if not found. - findShellRouteBranchDefaultLocation(branch); + findStatefulShellBranchDefaultLocation(branch); } else { final RouteBase defaultLocationRoute = matcher.findMatch(branch.defaultLocation!).last.route; @@ -150,7 +150,7 @@ class RouteConfiguration { } } } - _debugCheckShellRouteBranchDefaultLocations(route.routes, matcher); + _debugCheckStatefulShellBranchDefaultLocations(route.routes, matcher); } } on MatcherError catch (e) { assert( @@ -175,16 +175,16 @@ class RouteConfiguration { ancestor == route || _subRoutesRecursively(ancestor.routes).contains(route); - /// Recursively traverses the routes of the provided ShellRouteBranch to find - /// the first GoRoute, from which a full path will be derived. - String findShellRouteBranchDefaultLocation(StatefulShellBranch branch) { + /// Recursively traverses the routes of the provided StatefulShellBranch to + /// find the first GoRoute, from which a full path will be derived. + String findStatefulShellBranchDefaultLocation(StatefulShellBranch branch) { final GoRoute? route = _findFirstGoRoute(branch.routes); final String? defaultLocation = route != null ? _fullPathForRoute(route, '', routes) : null; assert( defaultLocation != null, - 'The default location of a ShellRouteBranch' - ' must be configured or derivable from GoRoute descendant'); + 'The default location of a StatefulShellBranch must be derivable from ' + 'GoRoute descendant'); return defaultLocation!; } diff --git a/packages/go_router/lib/src/misc/stateful_navigation_shell.dart b/packages/go_router/lib/src/misc/stateful_navigation_shell.dart index 14371612991b..65da9c58983c 100644 --- a/packages/go_router/lib/src/misc/stateful_navigation_shell.dart +++ b/packages/go_router/lib/src/misc/stateful_navigation_shell.dart @@ -38,6 +38,7 @@ typedef BranchNavigatorPreloadBuilder = Navigator Function( RouteMatchList navigatorMatchList, int startIndex, GlobalKey navigatorKey, + PopPageCallback onPopPage, String? restorationScopeId, ); @@ -98,6 +99,8 @@ class StatefulNavigationShellState extends State { List get _branches => widget.shellRoute.branches; + PopPageCallback get _onPopPage => widget.currentNavigator.onPopPage!; + Navigator? _navigatorForBranch(StatefulShellBranch branch) { return _navigatorCache[branch.navigatorKey]; } @@ -134,7 +137,7 @@ class StatefulNavigationShellState extends State { String _defaultBranchLocation(StatefulShellBranchState branchState) { String? defaultLocation = branchState.branch.defaultLocation; defaultLocation ??= widget.configuration - .findShellRouteBranchDefaultLocation(branchState.branch); + .findStatefulShellBranchDefaultLocation(branchState.branch); return defaultLocation; } @@ -178,6 +181,7 @@ class StatefulNavigationShellState extends State { matchList, shellRouteIndex + 1, branch.navigatorKey, + _onPopPage, branch.restorationScopeId, ); } diff --git a/packages/go_router/lib/src/route.dart b/packages/go_router/lib/src/route.dart index ae003ace168f..b2b4a975605f 100644 --- a/packages/go_router/lib/src/route.dart +++ b/packages/go_router/lib/src/route.dart @@ -489,12 +489,12 @@ class ShellRoute extends ShellRouteBase { /// implementing a UI with a [BottomNavigationBar], with a persistent navigation /// state for each tab. /// -/// A StatefulShellRoute is created by specifying a List of [ShellRouteBranch] +/// A StatefulShellRoute is created by specifying a List of [StatefulShellBranch] /// items, each representing a separate stateful branch in the route tree. -/// ShellRouteBranch provides the root routes and the Navigator key ([GlobalKey]) +/// StatefulShellBranch provides the root routes and the Navigator key ([GlobalKey]) /// for the branch, as well as an optional default location. /// -/// Like [ShellRoute], you can provide a [builder] and [pageBuilder] when +/// Like [ShellRoute], a [builder] and [pageBuilder] can be provided when /// creating a StatefulShellRoute. However, StatefulShellRoute differs in that /// the builder is mandatory and the pageBuilder may optionally be used in /// addition to the builder. The reason for this is that the builder function is @@ -563,7 +563,7 @@ class ShellRoute extends ShellRouteBase { /// }, /// branches: [ /// /// The first branch, i.e. tab 'A' -/// ShellRouteBranch( +/// StatefulShellBranch( /// navigatorKey: _tabANavigatorKey, /// routes: [ /// GoRoute( @@ -582,7 +582,7 @@ class ShellRoute extends ShellRouteBase { /// ], /// ), /// /// The second branch, i.e. tab 'B' -/// ShellRouteBranch( +/// StatefulShellBranch( /// navigatorKey: _tabBNavigatorKey, /// routes: [ /// GoRoute( @@ -606,11 +606,12 @@ class ShellRoute extends ShellRouteBase { /// ); /// ``` /// -/// When the [Page] for this route needs to be customized, you need to pass a -/// function for pageBuilder. Note that this page builder doesn't replace -/// the builder function, but instead receives the stateful shell built by +/// When the [Page] for this route needs to be customized, a pageBuilder needs +/// to be provided. Note that this page builder doesn't replace the builder +/// function, but instead receives the stateful shell built by /// [StatefulShellRoute] (using the builder function) as input. In other words, -/// you need to specify both when customizing a page. For example: +/// builder and pageBuilder must both be provided when using a custom page for +/// a StatefulShellRoute. For example: /// /// ``` /// final GoRouter _router = GoRouter( @@ -625,9 +626,9 @@ class ShellRoute extends ShellRouteBase { /// (BuildContext context, GoRouterState state, Widget statefulShell) { /// return NoTransitionPage(child: statefulShell); /// }, -/// branches: [ +/// branches: [ /// /// The first branch, i.e. root of tab 'A' -/// ShellRouteBranch(routes: [ +/// StatefulShellBranch(routes: [ /// GoRoute( /// parentNavigatorKey: _tabANavigatorKey, /// path: '/a', @@ -636,7 +637,7 @@ class ShellRoute extends ShellRouteBase { /// ), /// ]), /// /// The second branch, i.e. root of tab 'B' -/// ShellRouteBranch(routes: [ +/// StatefulShellBranch(routes: [ /// GoRoute( /// parentNavigatorKey: _tabBNavigatorKey, /// path: '/b', @@ -666,8 +667,8 @@ class StatefulShellRoute extends ShellRouteBase { /// /// A separate [Navigator] will be created for each of the branches, using /// the navigator key specified in [StatefulShellBranch]. Note that unlike - /// [ShellRoute], you must always provide a builder when creating - /// a StatefulShellRoute. The pageBuilder however is optional, and is used + /// [ShellRoute], a builder must always be provided when creating a + /// StatefulShellRoute. The pageBuilder however is optional, and is used /// in addition to the builder. StatefulShellRoute({ required this.branches, @@ -719,11 +720,13 @@ class StatefulShellRoute extends ShellRouteBase { /// Representation of a separate branch in a stateful navigation tree, used to /// configure [StatefulShellRoute]. /// -/// The only required argument when creating a ShellRouteBranch is the -/// sub-routes ([routes]), however in some cases you may also need to specify -/// the [defaultLocation], for instance of you're using another shell route as -/// direct sub-route. A [navigatorKey] can be useful to provide in case you need -/// to use the [Navigator] created for this branch elsewhere. +/// The only required argument when creating a StatefulShellBranch is the +/// sub-routes ([routes]), however sometimes it may be convenient to also +/// provide a [defaultLocation]. 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 [StatefulShellBranchState]). A [navigatorKey] +/// can be useful to provide in case it's necessary to access the [Navigator] +/// created for this branch elsewhere. @immutable class StatefulShellBranch { /// Constructs a [StatefulShellBranch]. @@ -751,8 +754,11 @@ class StatefulShellBranch { /// The default location for this route branch. /// - /// If none is specified, the first descendant [GoRoute] will be used (i.e. - /// first element in [routes], or a descendant). + /// If none is specified, the location of the first descendant [GoRoute] will + /// be used (i.e. first element in [routes], or a descendant). The default + /// location is used when loading the branch for the first time (for instance + /// when switching branch using the goBranch method in + /// [StatefulShellBranchState]). final String? defaultLocation; /// An optional name for this branch. diff --git a/packages/go_router/test/configuration_test.dart b/packages/go_router/test/configuration_test.dart index 8b49ebfa2ea1..88d155fbd0fb 100644 --- a/packages/go_router/test/configuration_test.dart +++ b/packages/go_router/test/configuration_test.dart @@ -393,7 +393,7 @@ void main() { }); test( - 'derives the correct defaultLocation for a ShellRouteBranch', + 'derives the correct defaultLocation for a StatefulShellBranch', () { final StatefulShellBranch branchA; final StatefulShellBranch branchY; @@ -467,9 +467,10 @@ void main() { }, ); - expect('/a', config.findShellRouteBranchDefaultLocation(branchA)); - expect('/a/x/y1', config.findShellRouteBranchDefaultLocation(branchY)); - expect('/b1', config.findShellRouteBranchDefaultLocation(branchB)); + expect('/a', config.findStatefulShellBranchDefaultLocation(branchA)); + expect( + '/a/x/y1', config.findStatefulShellBranchDefaultLocation(branchY)); + expect('/b1', config.findStatefulShellBranchDefaultLocation(branchB)); }, ); From 5b668f8bc10379cc06bb1c7d0d7c5894f04d30b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20Lo=CC=88fstrand?= Date: Thu, 5 Jan 2023 03:29:55 +0100 Subject: [PATCH 073/112] Fix for error in _preloadShellBranchNavigator due to recent changes in RouteBuilder. --- packages/go_router/lib/src/builder.dart | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/go_router/lib/src/builder.dart b/packages/go_router/lib/src/builder.dart index 786042365f5b..84a1237c93c8 100644 --- a/packages/go_router/lib/src/builder.dart +++ b/packages/go_router/lib/src/builder.dart @@ -281,10 +281,14 @@ class RouteBuilder { PopPageCallback onPopPage, String? restorationScopeId, ) { + final Map, List>> keyToPage = + , List>>{}; + _buildRecursive(context, matchList, startIndex, onPopPage, true, keyToPage, + navigatorKey, , GoRouterState>{}); + return _buildNavigator( onPopPage, - buildPages(context, matchList, startIndex, onPopPage, true, navigatorKey, - , GoRouterState>{}), + keyToPage[navigatorKey]!, navigatorKey, restorationScopeId: restorationScopeId, ); From 565c3cd9263199b4b53399b35aaa102d95947a87 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20Lo=CC=88fstrand?= Date: Thu, 19 Jan 2023 22:09:26 +0100 Subject: [PATCH 074/112] Updated _routeMatchLookUp to handle reused/cached Navigators. --- packages/go_router/lib/src/builder.dart | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/packages/go_router/lib/src/builder.dart b/packages/go_router/lib/src/builder.dart index 84a1237c93c8..8a46398bafee 100644 --- a/packages/go_router/lib/src/builder.dart +++ b/packages/go_router/lib/src/builder.dart @@ -52,17 +52,19 @@ class RouteBuilder { final GoRouterStateRegistry _registry = GoRouterStateRegistry(); - final Map, RouteMatch> _routeMatchLookUp = - , RouteMatch>{}; + final Expando _routeMatchLookUp = Expando( + 'Page to RouteMatch', + ); - /// Looks the the [RouteMatch] for a given [Page]. + /// Looks for the [RouteMatch] for a given [Page]. /// - /// The [Page] must be in the latest [Navigator.pages]; otherwise, this method - /// returns null. + /// The [Page] must have been previously built via this [RouteBuilder]; + /// otherwise, this method returns null. RouteMatch? getRouteMatchForPage(Page page) => _routeMatchLookUp[page]; - // final Map<> + void _setRouteMatchForPage(Page page, RouteMatch match) => + _routeMatchLookUp[page] = match; /// Builds the top-level Navigator for the given [RouteMatchList]. Widget build( @@ -71,7 +73,6 @@ class RouteBuilder { PopPageCallback onPopPage, bool routerNeglect, ) { - _routeMatchLookUp.clear(); if (matchList.isEmpty) { // The build method can be called before async redirect finishes. Build a // empty box until then. @@ -135,7 +136,6 @@ class RouteBuilder { GlobalKey navigatorKey, Map, GoRouterState> registry) { try { - assert(_routeMatchLookUp.isEmpty); final Map, List>> keyToPage = , List>>{}; _buildRecursive(context, matchList, startIndex, onPopPage, routerNeglect, @@ -143,7 +143,7 @@ class RouteBuilder { // Every Page should have a corresponding RouteMatch. assert(keyToPage.values.flattened - .every((Page page) => _routeMatchLookUp.containsKey(page))); + .every((Page page) => getRouteMatchForPage(page) != null)); return keyToPage[navigatorKey]!; } on _RouteBuilderError catch (e) { return >[ @@ -351,7 +351,7 @@ class RouteBuilder { page ??= buildPage(context, state, Builder(builder: (BuildContext context) { return _callRouteBuilder(context, state, match, childWidget: child); })); - _routeMatchLookUp[page] = match; + _setRouteMatchForPage(page, match); // Return the result of the route's builder() or pageBuilder() return page; From 5ee1a8fb7cbdae504e5cd4fa1923691a9282a8df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20Lo=CC=88fstrand?= Date: Thu, 19 Jan 2023 23:38:33 +0100 Subject: [PATCH 075/112] Added support for resetting state for single branch (resetBranch). --- .../src/misc/stateful_navigation_shell.dart | 49 ++++++++----- packages/go_router/lib/src/shell_state.dart | 68 +++++++++++++++---- packages/go_router/test/go_router_test.dart | 66 ++++++++++++++++++ 3 files changed, 151 insertions(+), 32 deletions(-) diff --git a/packages/go_router/lib/src/misc/stateful_navigation_shell.dart b/packages/go_router/lib/src/misc/stateful_navigation_shell.dart index 65da9c58983c..72cc231cf285 100644 --- a/packages/go_router/lib/src/misc/stateful_navigation_shell.dart +++ b/packages/go_router/lib/src/misc/stateful_navigation_shell.dart @@ -105,8 +105,11 @@ class StatefulNavigationShellState extends State { return _navigatorCache[branch.navigatorKey]; } - void _setNavigatorForBranch(StatefulShellBranch branch, Navigator navigator) { - _navigatorCache[branch.navigatorKey] = navigator; + void _setNavigatorForBranch( + StatefulShellBranch branch, Navigator? navigator) { + navigator != null + ? _navigatorCache[branch.navigatorKey] = navigator + : _navigatorCache.remove(branch.navigatorKey); } int _findCurrentIndex() { @@ -127,18 +130,17 @@ class StatefulNavigationShellState extends State { .then( (RouteMatchList matchList) => goRouter.routerDelegate.setNewRoutePath(matchList), - onError: (_) => goRouter.go(_defaultBranchLocation(branchState)), + onError: (_) => + goRouter.go(_defaultBranchLocation(branchState.branch)), ); } else { - goRouter.go(_defaultBranchLocation(branchState)); + goRouter.go(_defaultBranchLocation(branchState.branch)); } } - String _defaultBranchLocation(StatefulShellBranchState branchState) { - String? defaultLocation = branchState.branch.defaultLocation; - defaultLocation ??= widget.configuration - .findStatefulShellBranchDefaultLocation(branchState.branch); - return defaultLocation; + String _defaultBranchLocation(StatefulShellBranch branch) { + return branch.defaultLocation ?? + widget.configuration.findStatefulShellBranchDefaultLocation(branch); } void _preloadBranches() { @@ -163,8 +165,9 @@ class StatefulNavigationShellState extends State { GoRouter.of(context).routeInformationParser; final Future routeMatchList = parser.parseRouteInformationWithDependencies( - RouteInformation(location: _defaultBranchLocation(branchState)), - context); + RouteInformation(location: _defaultBranchLocation(branchState.branch)), + context, + ); StatefulShellBranchState createBranchNavigator(RouteMatchList matchList) { // Find the index of the branch root route in the match list @@ -255,12 +258,24 @@ class StatefulNavigationShellState extends State { _preloadBranches(); } - void _resetState() { - final StatefulShellBranchState currentBranchState = - _routeState.currentBranchState; - _navigatorCache.clear(); - _setupInitialStatefulShellRouteState(); - GoRouter.of(context).go(_defaultBranchLocation(currentBranchState)); + void _resetState( + StatefulShellBranchState? branchState, bool navigateToDefaultLocation) { + final StatefulShellBranch branch; + if (branchState != null) { + branch = branchState.branch; + _setNavigatorForBranch(branch, null); + _updateRouteBranchState( + _createStatefulShellBranchState(branch), + ); + } else { + branch = _routeState.currentBranchState.branch; + // Reset the state for all branches (the whole stateful shell) + _navigatorCache.clear(); + _setupInitialStatefulShellRouteState(); + } + if (navigateToDefaultLocation) { + GoRouter.of(context).go(_defaultBranchLocation(branch)); + } } StatefulShellBranchState _updateStatefulShellBranchState( diff --git a/packages/go_router/lib/src/shell_state.dart b/packages/go_router/lib/src/shell_state.dart index 330751dcf405..d78486e8cf25 100644 --- a/packages/go_router/lib/src/shell_state.dart +++ b/packages/go_router/lib/src/shell_state.dart @@ -27,7 +27,9 @@ class StatefulShellRouteState { required void Function( StatefulShellBranchState, UnmodifiableRouteMatchList?) switchActiveBranch, - required void Function() resetState, + required void Function(StatefulShellBranchState? branchState, + bool navigateToDefaultLocation) + resetState, }) : _switchActiveBranch = switchActiveBranch, _resetState = resetState; @@ -67,7 +69,9 @@ class StatefulShellRouteState { final void Function(StatefulShellBranchState, UnmodifiableRouteMatchList?) _switchActiveBranch; - final void Function() _resetState; + final void Function( + StatefulShellBranchState? branchState, bool navigateToDefaultLocation) + _resetState; /// Gets the [Widget]s representing each of the shell branches. /// @@ -80,19 +84,10 @@ class StatefulShellRouteState { List get children => branchStates.map((StatefulShellBranchState e) => e.child).toList(); - /// Navigate to the current location of the shell navigator with the provided - /// Navigator key, name or index. - /// - /// This method will switch the currently active [Navigator] for the - /// [StatefulShellRoute] by replacing the current navigation stack with the - /// one of the route branch identified by the provided Navigator key, name or - /// index. If resetLocation is true, the branch will be reset to its default - /// location (see [StatefulShellBranch.defaultLocation]). - void goBranch({ + StatefulShellBranchState _branchStateFor({ GlobalKey? navigatorKey, String? name, int? index, - bool resetLocation = false, }) { assert(navigatorKey != null || name != null || index != null); assert([navigatorKey, name, index].whereNotNull().length == 1); @@ -113,7 +108,28 @@ class StatefulShellRouteState { } else { state = branchStates[index!]; } + return state; + } + /// Navigate to the current location of the shell navigator with the provided + /// Navigator key, name or index. + /// + /// This method will switch the currently active [Navigator] for the + /// [StatefulShellRoute] by replacing the current navigation stack with the + /// one of the route branch identified by the provided Navigator key, name or + /// index. If resetLocation is true, the branch will be reset to its default + /// location (see [StatefulShellBranch.defaultLocation]). + void goBranch({ + GlobalKey? navigatorKey, + String? name, + int? index, + bool resetLocation = false, + }) { + final StatefulShellBranchState state = _branchStateFor( + navigatorKey: navigatorKey, + name: name, + index: index, + ); _switchActiveBranch(state, resetLocation ? null : state._matchList); } @@ -124,9 +140,31 @@ class StatefulShellRouteState { } /// Resets this StatefulShellRouteState by clearing all navigation state of - /// the branches, and returning the current branch to its default location. - void reset() { - _resetState(); + /// the branches + /// + /// After the state has been reset, the current branch will navigated to its + /// default location, if [navigateToDefaultLocation] is true. + void reset({bool navigateToDefaultLocation = true}) { + _resetState(null, navigateToDefaultLocation); + } + + /// Resets the navigation state of the branch identified by the provided + /// Navigator key, name or index. + /// + /// After the state has been reset, the branch will navigated to its + /// default location, if [navigateToDefaultLocation] is true. + void resetBranch({ + GlobalKey? navigatorKey, + String? name, + int? index, + bool navigateToDefaultLocation = true, + }) { + final StatefulShellBranchState state = _branchStateFor( + navigatorKey: navigatorKey, + name: name, + index: index, + ); + _resetState(state, navigateToDefaultLocation); } @override diff --git a/packages/go_router/test/go_router_test.dart b/packages/go_router/test/go_router_test.dart index 1735c57f6e8d..489ed03192d3 100644 --- a/packages/go_router/test/go_router_test.dart +++ b/packages/go_router/test/go_router_test.dart @@ -3559,6 +3559,72 @@ void main() { expect(find.text('Screen A'), findsOneWidget); expect(find.text('Screen A Detail'), findsNothing); }); + + testWidgets('Single branch of StatefulShellRoute is correctly reset', + (WidgetTester tester) async { + final GlobalKey rootNavigatorKey = + GlobalKey(); + StatefulShellRouteState? routeState; + + final List routes = [ + StatefulShellRoute( + builder: (BuildContext context, GoRouterState state, Widget child) { + routeState = StatefulShellRouteState.of(context); + return child; + }, + branches: [ + StatefulShellBranch(routes: [ + GoRoute( + path: '/a', + builder: (BuildContext context, GoRouterState state) => + const Text('Screen A'), + routes: [ + GoRoute( + path: 'detail', + builder: (BuildContext context, GoRouterState state) => + const Text('Screen A Detail'), + ), + ], + ), + ]), + StatefulShellBranch(routes: [ + GoRoute( + path: '/b', + builder: (BuildContext context, GoRouterState state) => + const Text('Screen B'), + routes: [ + GoRoute( + path: 'detail', + builder: (BuildContext context, GoRouterState state) => + const Text('Screen B Detail'), + ), + ], + ), + ]), + ], + ), + ]; + + final GoRouter router = await createRouter(routes, tester, + initialLocation: '/a/detail', navigatorKey: rootNavigatorKey); + expect(find.text('Screen A'), findsNothing); + expect(find.text('Screen A Detail'), findsOneWidget); + + router.go('/b/detail'); + await tester.pumpAndSettle(); + expect(find.text('Screen B'), findsNothing); + expect(find.text('Screen B Detail'), findsOneWidget); + + routeState!.resetBranch(index: 1); + await tester.pumpAndSettle(); + expect(find.text('Screen B'), findsOneWidget); + expect(find.text('Screen B Detail'), findsNothing); + + routeState!.goBranch(index: 0); + await tester.pumpAndSettle(); + expect(find.text('Screen A'), findsNothing); + expect(find.text('Screen A Detail'), findsOneWidget); + }); }); group('Imperative navigation', () { From 358551f06f795f61c3ac94227c65ba277de3015b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20Lo=CC=88fstrand?= Date: Fri, 20 Jan 2023 00:58:43 +0100 Subject: [PATCH 076/112] Added examples of pushing modal routes above the stateful shell. --- .../example/lib/stateful_shell_route.dart | 76 +++++++++++++++++++ 1 file changed, 76 insertions(+) diff --git a/packages/go_router/example/lib/stateful_shell_route.dart b/packages/go_router/example/lib/stateful_shell_route.dart index a838b951d75a..893205045292 100644 --- a/packages/go_router/example/lib/stateful_shell_route.dart +++ b/packages/go_router/example/lib/stateful_shell_route.dart @@ -6,6 +6,8 @@ import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; +final GlobalKey _rootNavigatorKey = + GlobalKey(debugLabel: 'root'); final GlobalKey _tabANavigatorKey = GlobalKey(debugLabel: 'tabANav'); @@ -29,8 +31,15 @@ class NestedTabNavigationExampleApp extends StatelessWidget { NestedTabNavigationExampleApp({Key? key}) : super(key: key); final GoRouter _router = GoRouter( + navigatorKey: _rootNavigatorKey, initialLocation: '/a', routes: [ + GoRoute( + path: '/modal', + parentNavigatorKey: _rootNavigatorKey, + builder: (BuildContext context, GoRouterState state) => + const ModalScreen(), + ), StatefulShellRoute( branches: [ /// The route branch for the first tab of the bottom navigation bar. @@ -276,6 +285,43 @@ class RootScreen extends StatelessWidget { }, child: const Text('View more details'), ), + const Padding(padding: EdgeInsets.all(8)), + ElevatedButton( + onPressed: () { + GoRouter.of(context).push('/modal'); + }, + child: const Text('Show modal screen on root navigator'), + ), + const Padding(padding: EdgeInsets.all(4)), + ElevatedButton( + onPressed: () { + showModalBottomSheet( + context: context, + useRootNavigator: true, + builder: _bottomSheet); + }, + child: const Text('Show bottom sheet on root navigator'), + ), + ], + ), + ), + ); + } + + Widget _bottomSheet(BuildContext context) { + return Container( + height: 200, + color: Colors.amber, + child: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + mainAxisSize: MainAxisSize.min, + children: [ + const Text('Modal BottomSheet'), + ElevatedButton( + child: const Text('Close BottomSheet'), + onPressed: () => Navigator.pop(context), + ), ], ), ), @@ -371,6 +417,36 @@ class DetailsScreenState extends State { } } +/// Widget for a modal screen. +class ModalScreen extends StatelessWidget { + /// Creates a ModalScreen + const ModalScreen({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('Modal'), + ), + body: Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text('Modal screen', style: Theme.of(context).textTheme.titleLarge), + const Padding(padding: EdgeInsets.all(8)), + ElevatedButton( + onPressed: () { + GoRouter.of(context).go('/a'); + }, + child: const Text('Go to initial section'), + ), + ], + ), + ), + ); + } +} + /// Builds a nested shell using a [TabBar] and [TabBarView]. class TabbedRootScreen extends StatelessWidget { /// Constructs a TabbedRootScreen From 87211aa9ebbb19359a57676c9c1a95534aca3bdf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20Lo=CC=88fstrand?= Date: Tue, 7 Feb 2023 23:00:06 +0100 Subject: [PATCH 077/112] Alternative StatefulShellRoute implementation, were either a builder or pageBuilder can be provided, much like ShellRoute/GoRoute, but with different parameters (StatefulShellFactory). --- .../example/lib/stateful_shell_route.dart | 67 +++++++------ packages/go_router/lib/go_router.dart | 5 +- packages/go_router/lib/src/builder.dart | 18 ++-- .../src/misc/stateful_navigation_shell.dart | 26 ++++- packages/go_router/lib/src/route.dart | 32 ++++++- packages/go_router/lib/src/typedefs.dart | 23 ++++- packages/go_router/test/builder_test.dart | 13 ++- .../go_router/test/configuration_test.dart | 25 +++-- packages/go_router/test/delegate_test.dart | 4 +- packages/go_router/test/go_router_test.dart | 95 +++++++++++-------- packages/go_router/test/test_helpers.dart | 5 + 11 files changed, 212 insertions(+), 101 deletions(-) diff --git a/packages/go_router/example/lib/stateful_shell_route.dart b/packages/go_router/example/lib/stateful_shell_route.dart index 893205045292..4706f580a77e 100644 --- a/packages/go_router/example/lib/stateful_shell_route.dart +++ b/packages/go_router/example/lib/stateful_shell_route.dart @@ -153,22 +153,25 @@ class NestedTabNavigationExampleApp extends StatelessWidget { ), ]), ], - builder: - (BuildContext context, GoRouterState state, Widget child) { + builder: (StatefulShellFactory shellFactory) { /// For this nested StatefulShellRoute we are using a custom /// container (TabBarView) for the branch navigators, and thus /// ignoring the default navigator contained passed to the /// builder. Custom implementation can access the branch /// navigators via the StatefulShellRouteState /// (see TabbedRootScreen for details). - return const TabbedRootScreen(); + return shellFactory.buildShell((BuildContext context, + GoRouterState state, Widget child) => + const TabbedRootScreen()); }, ), ], ), ], - builder: (BuildContext context, GoRouterState state, Widget child) { - return ScaffoldWithNavBar(body: child); + builder: (StatefulShellFactory shellFactory) { + return shellFactory.buildShell( + (BuildContext context, GoRouterState state, Widget child) => + ScaffoldWithNavBar(body: child)); }, /// It's possible to customize the container for the branch navigators @@ -179,16 +182,19 @@ class NestedTabNavigationExampleApp extends StatelessWidget { /// tree. Instead, access the child widgets of each branch directly /// (see StatefulShellRouteState.children) to implement a custom layout /// and container for the navigators. - // builder: (BuildContext context, GoRouterState state, Widget child) { - // return ScaffoldWithNavBar( - // body: _AnimatedRouteBranchContainer(), + // builder: (StatefulShellFactory shellFactory) { + // return shellFactory.buildShell((BuildContext context, GoRouterState state, Widget child) => + // ScaffoldWithNavBar( + // body: _AnimatedRouteBranchContainer(), + // ), // ); // }, /// If it's necessary to customize the Page for StatefulShellRoute, /// provide a pageBuilder function in addition to the builder, for example: - // pageBuilder: - // (BuildContext context, GoRouterState state, Widget statefulShell) { + // pageBuilder: (StatefulShellFactory shellFactory) { + // final Widget statefulShell = shellFactory.buildShell((BuildContext context, + // GoRouterState state, Widget child) => ScaffoldWithNavBar(body: child)); // return NoTransitionPage(child: statefulShell); // }, ), @@ -529,22 +535,29 @@ class _AnimatedRouteBranchContainer extends StatelessWidget { final StatefulShellRouteState shellRouteState = StatefulShellRouteState.of(context); final int currentIndex = shellRouteState.currentIndex; - return Stack( - children: shellRouteState.children.mapIndexed( - (int index, Widget? navigator) { - return AnimatedScale( - scale: index == currentIndex ? 1 : 1.5, - duration: const Duration(milliseconds: 400), - child: AnimatedOpacity( - opacity: index == currentIndex ? 1 : 0, - duration: const Duration(milliseconds: 400), - child: Offstage( - offstage: index != currentIndex, - child: navigator ?? const SizedBox.shrink(), - ), - ), - ); - }, - ).toList()); + + final PageController controller = PageController(initialPage: currentIndex); + return PageView( + controller: controller, + children: shellRouteState.children, + ); + + // // return Stack( + // // children: shellRouteState.children.mapIndexed( + // // (int index, Widget? navigator) { + // // return AnimatedScale( + // // scale: index == currentIndex ? 1 : 1.5, + // // duration: const Duration(milliseconds: 400), + // // child: AnimatedOpacity( + // // opacity: index == currentIndex ? 1 : 0, + // // duration: const Duration(milliseconds: 400), + // // child: Offstage( + // // offstage: index != currentIndex, + // // child: navigator ?? const SizedBox.shrink(), + // // ), + // // ), + // // ); + // // }, + // ).toList()); } } diff --git a/packages/go_router/lib/go_router.dart b/packages/go_router/lib/go_router.dart index 434a57c7bba4..0c759b305a9e 100644 --- a/packages/go_router/lib/go_router.dart +++ b/packages/go_router/lib/go_router.dart @@ -26,6 +26,9 @@ export 'src/typedefs.dart' GoRouterPageBuilder, GoRouterRedirect, GoRouterWidgetBuilder, + StatefulShellFactory, ShellRouteBuilder, ShellRoutePageBuilder, - StatefulShellBranchBuilder; + ShellBodyWidgetBuilder, + StatefulShellRouteBuilder, + StatefulShellRoutePageBuilder; diff --git a/packages/go_router/lib/src/builder.dart b/packages/go_router/lib/src/builder.dart index 8a46398bafee..064c61c568d0 100644 --- a/packages/go_router/lib/src/builder.dart +++ b/packages/go_router/lib/src/builder.dart @@ -267,6 +267,8 @@ class RouteBuilder { configuration: configuration, shellRoute: shellRoute, shellGoRouterState: shellRouterState, + shellBodyWidgetBuilder: (BuildContext _, GoRouterState __, Widget ___) => + throw _RouteBuilderError('ShellWidgetBuilder not configured'), currentNavigator: navigator, currentMatchList: currentMatchList.unmodifiableRouteMatchList(), branchPreloadNavigatorBuilder: _preloadShellBranchNavigator, @@ -381,19 +383,13 @@ class RouteBuilder { 'Attempt to build ShellRoute without a child widget'); } - if (route is StatefulShellRoute) { - // StatefulShellRoute builder will already have been called at this - // point, to create childWidget - return childWidget; - } else if (route is ShellRoute) { - final ShellRouteBuilder? builder = route.builder; - - if (builder == null) { - throw _RouteBuilderError('No builder provided to ShellRoute: $route'); - } + final ShellRouteBuilder? builder = route.builder; - return builder(context, state, childWidget); + if (builder == null) { + throw _RouteBuilderError('No builder provided to ShellRoute: $route'); } + + return builder(context, state, childWidget); } throw _RouteBuilderException('Unsupported route type $route'); diff --git a/packages/go_router/lib/src/misc/stateful_navigation_shell.dart b/packages/go_router/lib/src/misc/stateful_navigation_shell.dart index 72cc231cf285..9bf41040505b 100644 --- a/packages/go_router/lib/src/misc/stateful_navigation_shell.dart +++ b/packages/go_router/lib/src/misc/stateful_navigation_shell.dart @@ -57,12 +57,14 @@ typedef BranchNavigatorPreloadBuilder = Navigator Function( /// However, implementors can choose to disregard this and use an alternate /// container around the branch navigators /// (see [StatefulShellRouteState.children]) instead. -class StatefulNavigationShell extends StatefulWidget { +class StatefulNavigationShell extends StatefulWidget + implements StatefulShellFactory { /// Constructs an [StatefulNavigationShell]. const StatefulNavigationShell({ required this.configuration, required this.shellRoute, required this.shellGoRouterState, + required this.shellBodyWidgetBuilder, required this.currentNavigator, required this.currentMatchList, required this.branchPreloadNavigatorBuilder, @@ -78,6 +80,9 @@ class StatefulNavigationShell extends StatefulWidget { /// The [GoRouterState] for the navigation shell. final GoRouterState shellGoRouterState; + /// The shell body widget builder. + final ShellBodyWidgetBuilder shellBodyWidgetBuilder; + /// The navigator for the currently active route branch final Navigator currentNavigator; @@ -89,6 +94,20 @@ class StatefulNavigationShell extends StatefulWidget { @override State createState() => StatefulNavigationShellState(); + + @override + Widget buildShell(ShellBodyWidgetBuilder shellWidgetBuilder) { + return StatefulNavigationShell( + configuration: configuration, + shellRoute: shellRoute, + shellGoRouterState: shellGoRouterState, + shellBodyWidgetBuilder: shellWidgetBuilder, + currentNavigator: currentNavigator, + currentMatchList: currentMatchList, + branchPreloadNavigatorBuilder: branchPreloadNavigatorBuilder, + key: key, + ); + } } /// State for StatefulNavigationShell. @@ -371,8 +390,9 @@ class StatefulNavigationShellState extends State { child: Builder(builder: (BuildContext context) { // This Builder Widget is mainly used to make it possible to access the // StatefulShellRouteState via the BuildContext in the ShellRouteBuilder - final ShellRouteBuilder shellRouteBuilder = widget.shellRoute.builder!; - return shellRouteBuilder( + final ShellBodyWidgetBuilder shellWidgetBuilder = + widget.shellBodyWidgetBuilder; + return shellWidgetBuilder( context, widget.shellGoRouterState, _IndexedStackedRouteBranchContainer(routeState: _routeState), diff --git a/packages/go_router/lib/src/route.dart b/packages/go_router/lib/src/route.dart index b2b4a975605f..a28770ce2958 100644 --- a/packages/go_router/lib/src/route.dart +++ b/packages/go_router/lib/src/route.dart @@ -672,12 +672,17 @@ class StatefulShellRoute extends ShellRouteBase { /// in addition to the builder. StatefulShellRoute({ required this.branches, - required super.builder, - super.pageBuilder, + StatefulShellRouteBuilder? builder, + StatefulShellRoutePageBuilder? pageBuilder, }) : assert(branches.isNotEmpty), + assert((pageBuilder != null) ^ (builder != null), + 'builder or pageBuilder must be provided'), assert(_debugUniqueNavigatorKeys(branches).length == branches.length, 'Navigator keys must be unique'), - super._(routes: _routes(branches)) { + super._( + builder: _builder(builder), + pageBuilder: _pageBuilder(pageBuilder), + routes: _routes(branches)) { for (int i = 0; i < routes.length; ++i) { final RouteBase route = routes[i]; if (route is GoRoute) { @@ -687,6 +692,27 @@ class StatefulShellRoute extends ShellRouteBase { } } + static ShellRouteBuilder? _builder(StatefulShellRouteBuilder? builder) { + if (builder == null) { + return null; + } + return (BuildContext context, GoRouterState state, Widget child) { + assert(child is StatefulShellFactory); + return builder(child as StatefulShellFactory); + }; + } + + static ShellRoutePageBuilder? _pageBuilder( + StatefulShellRoutePageBuilder? builder) { + if (builder == null) { + return null; + } + return (BuildContext context, GoRouterState state, Widget child) { + assert(child is StatefulShellFactory); + return builder(child as StatefulShellFactory); + }; + } + /// Representations of the different stateful route branches that this /// shell route will manage. /// diff --git a/packages/go_router/lib/src/typedefs.dart b/packages/go_router/lib/src/typedefs.dart index e7f1b2923513..e818a22df9dc 100644 --- a/packages/go_router/lib/src/typedefs.dart +++ b/packages/go_router/lib/src/typedefs.dart @@ -34,10 +34,25 @@ typedef ShellRoutePageBuilder = Page Function( Widget child, ); -/// The branch builder for a [StatefulShellRoute]. -typedef StatefulShellBranchBuilder = List Function( - BuildContext context, - GoRouterState state, +/// The shell body widget builder for [StatefulShellRoute]. +typedef ShellBodyWidgetBuilder = Widget Function( + BuildContext context, GoRouterState state, Widget child); + +/// The factory for building the shell of a [StatefulShellRoute]. +abstract class StatefulShellFactory { + /// Builds the shell of a [StatefulShellRoute], using the provided builder to + /// build the body. + Widget buildShell(ShellBodyWidgetBuilder shellBodyWidgetBuilder); +} + +/// The widget builder for [StatefulShellRoute]. +typedef StatefulShellRouteBuilder = Widget Function( + StatefulShellFactory statefulNavigation, +); + +/// The page builder for [StatefulShellRoute]. +typedef StatefulShellRoutePageBuilder = Page Function( + StatefulShellFactory statefulNavigation, ); /// The signature of the navigatorBuilder callback. diff --git a/packages/go_router/test/builder_test.dart b/packages/go_router/test/builder_test.dart index 01b684cab630..3ed1047289c1 100644 --- a/packages/go_router/test/builder_test.dart +++ b/packages/go_router/test/builder_test.dart @@ -9,6 +9,9 @@ import 'package:go_router/src/configuration.dart'; import 'package:go_router/src/match.dart'; import 'package:go_router/src/matching.dart'; import 'package:go_router/src/router.dart'; +import 'package:go_router/src/typedefs.dart'; + +import 'test_helpers.dart'; void main() { group('RouteBuilder', () { @@ -101,7 +104,7 @@ void main() { final RouteConfiguration config = RouteConfiguration( routes: [ StatefulShellRoute( - builder: (_, __, Widget child) => child, + builder: (StatefulShellFactory factory) => factory.dummy(), branches: [ StatefulShellBranch(navigatorKey: key, routes: [ GoRoute( @@ -153,7 +156,7 @@ void main() { const Text('Root'), routes: [ shell = StatefulShellRoute( - builder: (_, __, Widget child) => child, + builder: (StatefulShellFactory factory) => factory.dummy(), branches: [ StatefulShellBranch(navigatorKey: key, routes: [ nested = GoRoute( @@ -577,8 +580,10 @@ void main() { navigatorKey: rootNavigatorKey, routes: [ StatefulShellRoute( - builder: (BuildContext context, GoRouterState state, Widget child) { - return _HomeScreen(child: child); + builder: (StatefulShellFactory factory) { + return factory.buildShell( + (BuildContext context, GoRouterState state, Widget child) => + _HomeScreen(child: child)); }, branches: [ StatefulShellBranch( diff --git a/packages/go_router/test/configuration_test.dart b/packages/go_router/test/configuration_test.dart index 88d155fbd0fb..66f5d64d730b 100644 --- a/packages/go_router/test/configuration_test.dart +++ b/packages/go_router/test/configuration_test.dart @@ -5,6 +5,7 @@ import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:go_router/src/configuration.dart'; +import 'package:go_router/src/typedefs.dart'; void main() { group('RouteConfiguration', () { @@ -114,7 +115,7 @@ void main() { parentNavigatorKey: someNavigatorKey), ], ), - ], builder: _mockShellBuilder), + ], builder: _mockStatefulShellBuilder), ], redirectLimit: 10, topRedirect: (BuildContext context, GoRouterState state) { @@ -144,7 +145,7 @@ void main() { routes: [ StatefulShellRoute(branches: [ StatefulShellBranch(routes: shellRouteChildren) - ], builder: _mockShellBuilder), + ], builder: _mockStatefulShellBuilder), ], redirectLimit: 10, topRedirect: (BuildContext context, GoRouterState state) { @@ -185,7 +186,7 @@ void main() { StatefulShellBranch( routes: [routeB], navigatorKey: sectionBNavigatorKey), - ], builder: _mockShellBuilder), + ], builder: _mockStatefulShellBuilder), ], redirectLimit: 10, topRedirect: (BuildContext context, GoRouterState state) { @@ -231,7 +232,7 @@ void main() { ), ], ), - ], builder: _mockShellBuilder), + ], builder: _mockStatefulShellBuilder), ], redirectLimit: 10, topRedirect: (BuildContext context, GoRouterState state) { @@ -281,10 +282,10 @@ void main() { ), ], ), - ], builder: _mockShellBuilder), + ], builder: _mockStatefulShellBuilder), ], ), - ], builder: _mockShellBuilder), + ], builder: _mockStatefulShellBuilder), ], redirectLimit: 10, topRedirect: (BuildContext context, GoRouterState state) { @@ -364,7 +365,7 @@ void main() { ]), ], ), - ], builder: _mockShellBuilder), + ], builder: _mockStatefulShellBuilder), ], ), StatefulShellBranch(routes: [ @@ -383,7 +384,7 @@ void main() { ], ), ]), - ], builder: _mockShellBuilder), + ], builder: _mockStatefulShellBuilder), ], redirectLimit: 10, topRedirect: (BuildContext context, GoRouterState state) { @@ -403,7 +404,7 @@ void main() { navigatorKey: GlobalKey(debugLabel: 'root'), routes: [ StatefulShellRoute( - builder: (_, __, Widget child) => child, + builder: _mockStatefulShellBuilder, branches: [ branchA = StatefulShellBranch(routes: [ GoRoute( @@ -415,7 +416,7 @@ void main() { builder: _mockScreenBuilder, routes: [ StatefulShellRoute( - builder: (_, __, Widget child) => child, + builder: _mockStatefulShellBuilder, branches: [ branchY = StatefulShellBranch(routes: [ @@ -912,3 +913,7 @@ Widget _mockScreenBuilder(BuildContext context, GoRouterState state) => Widget _mockShellBuilder( BuildContext context, GoRouterState state, Widget child) => child; + +Widget _mockStatefulShellBuilder(StatefulShellFactory factory) => + factory.buildShell( + (BuildContext context, GoRouterState state, Widget child) => child); diff --git a/packages/go_router/test/delegate_test.dart b/packages/go_router/test/delegate_test.dart index 6e664b23a1db..fefe9ee3c6c6 100644 --- a/packages/go_router/test/delegate_test.dart +++ b/packages/go_router/test/delegate_test.dart @@ -9,6 +9,8 @@ import 'package:go_router/src/delegate.dart'; import 'package:go_router/src/match.dart'; import 'package:go_router/src/misc/error_screen.dart'; +import 'test_helpers.dart'; + Future createGoRouter( WidgetTester tester, { Listenable? refreshListenable, @@ -62,7 +64,7 @@ Future createGoRouterWithStatefulShellRoute( builder: (_, __) => const DummyStatefulWidget()), ]), ]), - ], builder: (_, __, Widget child) => child), + ], builder: (StatefulShellFactory factory) => factory.dummy()), ], ); await tester.pumpWidget(MaterialApp.router( diff --git a/packages/go_router/test/go_router_test.dart b/packages/go_router/test/go_router_test.dart index 489ed03192d3..16fe2ec50d2c 100644 --- a/packages/go_router/test/go_router_test.dart +++ b/packages/go_router/test/go_router_test.dart @@ -2403,9 +2403,12 @@ void main() { StatefulShellRouteState? routeState; final List routes = [ StatefulShellRoute( - builder: (BuildContext context, _, Widget child) { - routeState = StatefulShellRouteState.of(context); - return child; + builder: (StatefulShellFactory factory) { + return factory.buildShell( + (BuildContext context, GoRouterState state, Widget child) { + routeState = StatefulShellRouteState.of(context); + return child; + }); }, branches: [ StatefulShellBranch( @@ -2924,9 +2927,12 @@ void main() { final List routes = [ StatefulShellRoute( - builder: (BuildContext context, GoRouterState state, Widget child) { - routeState = StatefulShellRouteState.of(context); - return child; + builder: (StatefulShellFactory factory) { + return factory.buildShell( + (BuildContext context, GoRouterState state, Widget child) { + routeState = StatefulShellRouteState.of(context); + return child; + }); }, branches: [ StatefulShellBranch( @@ -3032,9 +3038,12 @@ void main() { final List routes = [ StatefulShellRoute( - builder: (BuildContext context, GoRouterState state, Widget child) { - routeState = StatefulShellRouteState.of(context); - return child; + builder: (StatefulShellFactory factory) { + return factory.buildShell( + (BuildContext context, GoRouterState state, Widget child) { + routeState = StatefulShellRouteState.of(context); + return child; + }); }, branches: [ StatefulShellBranch(routes: [ @@ -3104,9 +3113,12 @@ void main() { final List routes = [ StatefulShellRoute( - builder: (BuildContext context, GoRouterState state, Widget child) { - routeState = StatefulShellRouteState.of(context); - return child; + builder: (StatefulShellFactory factory) { + return factory.buildShell( + (BuildContext context, GoRouterState state, Widget child) { + routeState = StatefulShellRouteState.of(context); + return child; + }); }, branches: [ StatefulShellBranch( @@ -3188,9 +3200,12 @@ void main() { final List routes = [ StatefulShellRoute( - builder: (BuildContext context, _, Widget child) { - routeState = StatefulShellRouteState.of(context); - return child; + builder: (StatefulShellFactory factory) { + return factory.buildShell( + (BuildContext context, GoRouterState state, Widget child) { + routeState = StatefulShellRouteState.of(context); + return child; + }); }, branches: [ StatefulShellBranch(routes: [ @@ -3246,9 +3261,12 @@ void main() { Text('Common - ${state.extra}'), ), StatefulShellRoute( - builder: (BuildContext context, GoRouterState state, Widget child) { - routeState = StatefulShellRouteState.of(context); - return child; + builder: (StatefulShellFactory factory) { + return factory.buildShell( + (BuildContext context, GoRouterState state, Widget child) { + routeState = StatefulShellRouteState.of(context); + return child; + }); }, branches: [ StatefulShellBranch(routes: [ @@ -3312,8 +3330,7 @@ void main() { final List routes = [ StatefulShellRoute( - builder: (BuildContext context, GoRouterState state, Widget child) => - child, + builder: (StatefulShellFactory factory) => factory.dummy(), branches: [ StatefulShellBranch(routes: [ GoRoute( @@ -3332,8 +3349,7 @@ void main() { ], ), StatefulShellRoute( - builder: (BuildContext context, GoRouterState state, Widget child) => - Scaffold(body: child), + builder: (StatefulShellFactory factory) => factory.dummy(), branches: [ StatefulShellBranch( preload: true, @@ -3408,9 +3424,12 @@ void main() { final List routes = [ StatefulShellRoute( - builder: (BuildContext context, GoRouterState state, Widget child) { - routeState = StatefulShellRouteState.of(context); - return child; + builder: (StatefulShellFactory factory) { + return factory.buildShell( + (BuildContext context, GoRouterState state, Widget child) { + routeState = StatefulShellRouteState.of(context); + return child; + }); }, branches: [ StatefulShellBranch(routes: [ @@ -3502,9 +3521,12 @@ void main() { final List routes = [ StatefulShellRoute( - builder: (BuildContext context, GoRouterState state, Widget child) { - routeState = StatefulShellRouteState.of(context); - return child; + builder: (StatefulShellFactory factory) { + return factory.buildShell( + (BuildContext context, GoRouterState state, Widget child) { + routeState = StatefulShellRouteState.of(context); + return child; + }); }, branches: [ StatefulShellBranch(routes: [ @@ -3568,9 +3590,12 @@ void main() { final List routes = [ StatefulShellRoute( - builder: (BuildContext context, GoRouterState state, Widget child) { - routeState = StatefulShellRouteState.of(context); - return child; + builder: (StatefulShellFactory factory) { + return factory.buildShell( + (BuildContext context, GoRouterState state, Widget child) { + routeState = StatefulShellRouteState.of(context); + return child; + }); }, branches: [ StatefulShellBranch(routes: [ @@ -3754,12 +3779,8 @@ void main() { initialLocation: '/a', routes: [ StatefulShellRoute( - builder: - (BuildContext context, GoRouterState state, Widget child) { - return Scaffold( - appBar: AppBar(title: const Text('Shell')), - body: child, - ); + builder: (StatefulShellFactory factory) { + return factory.dummy(); }, branches: [ StatefulShellBranch(routes: [ diff --git a/packages/go_router/test/test_helpers.dart b/packages/go_router/test/test_helpers.dart index 9b80ea46ff61..1fa95e38eb9d 100644 --- a/packages/go_router/test/test_helpers.dart +++ b/packages/go_router/test/test_helpers.dart @@ -242,3 +242,8 @@ Future simulateAndroidBackButton(WidgetTester tester) async { await tester.binding.defaultBinaryMessenger .handlePlatformMessage('flutter/navigation', message, (_) {}); } + +extension StatefulShellFactoryHelper on StatefulShellFactory { + Widget dummy() => buildShell( + (BuildContext context, GoRouterState state, Widget child) => child); +} From 257a27258cd8a1d8766518fa6f0576ec7ead6731 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20Lo=CC=88fstrand?= Date: Thu, 16 Feb 2023 15:28:51 +0100 Subject: [PATCH 078/112] Extracted construction of StatefulNavigationShell and Navigators out from RouteBuilder. Also reduced interdependencies (from StatefulNavigationShell etc), primarily by using GoRouterState instead of RouteMatchList. --- .../example/lib/stateful_shell_route.dart | 41 +++-- packages/go_router/lib/go_router.dart | 7 +- packages/go_router/lib/src/builder.dart | 159 +++++++---------- packages/go_router/lib/src/delegate.dart | 2 +- packages/go_router/lib/src/matching.dart | 71 ++++++-- .../src/misc/stateful_navigation_shell.dart | 166 ++++++------------ .../go_router/lib/src/navigator_builder.dart | 99 +++++++++++ packages/go_router/lib/src/redirection.dart | 2 + packages/go_router/lib/src/route.dart | 122 +++++++++---- packages/go_router/lib/src/router.dart | 10 +- packages/go_router/lib/src/shell_state.dart | 34 ++-- packages/go_router/lib/src/state.dart | 68 +++++-- packages/go_router/lib/src/typedefs.dart | 31 ++-- packages/go_router/test/builder_test.dart | 12 +- .../go_router/test/configuration_test.dart | 27 ++- packages/go_router/test/delegate_test.dart | 2 +- packages/go_router/test/go_router_test.dart | 98 +++++++---- packages/go_router/test/match_test.dart | 6 +- packages/go_router/test/test_helpers.dart | 12 +- 19 files changed, 566 insertions(+), 403 deletions(-) create mode 100644 packages/go_router/lib/src/navigator_builder.dart diff --git a/packages/go_router/example/lib/stateful_shell_route.dart b/packages/go_router/example/lib/stateful_shell_route.dart index 4706f580a77e..780310cf71ba 100644 --- a/packages/go_router/example/lib/stateful_shell_route.dart +++ b/packages/go_router/example/lib/stateful_shell_route.dart @@ -153,25 +153,33 @@ class NestedTabNavigationExampleApp extends StatelessWidget { ), ]), ], - builder: (StatefulShellFactory shellFactory) { + builder: (BuildContext context, GoRouterState state, + ShellNavigatorBuilder navigatorBuilder) { /// For this nested StatefulShellRoute we are using a custom /// container (TabBarView) for the branch navigators, and thus /// ignoring the default navigator contained passed to the /// builder. Custom implementation can access the branch /// navigators via the StatefulShellRouteState /// (see TabbedRootScreen for details). - return shellFactory.buildShell((BuildContext context, - GoRouterState state, Widget child) => - const TabbedRootScreen()); + return navigatorBuilder.buildStatefulShell( + context, + state, + (BuildContext context, GoRouterState state, Widget child) => + const TabbedRootScreen(), + ); }, ), ], ), ], - builder: (StatefulShellFactory shellFactory) { - return shellFactory.buildShell( - (BuildContext context, GoRouterState state, Widget child) => - ScaffoldWithNavBar(body: child)); + builder: (BuildContext context, GoRouterState state, + ShellNavigatorBuilder navigatorBuilder) { + return navigatorBuilder.buildStatefulShell( + context, + state, + (BuildContext context, GoRouterState state, Widget child) => + ScaffoldWithNavBar(body: child), + ); }, /// It's possible to customize the container for the branch navigators @@ -182,19 +190,18 @@ class NestedTabNavigationExampleApp extends StatelessWidget { /// tree. Instead, access the child widgets of each branch directly /// (see StatefulShellRouteState.children) to implement a custom layout /// and container for the navigators. - // builder: (StatefulShellFactory shellFactory) { - // return shellFactory.buildShell((BuildContext context, GoRouterState state, Widget child) => - // ScaffoldWithNavBar( - // body: _AnimatedRouteBranchContainer(), - // ), - // ); + // builder: (BuildContext context, GoRouterState state, ShellNavigatorBuilder navigatorBuilder) { + // return navigatorBuilder.buildStatefulShell(context, state, + // (BuildContext context, GoRouterState state, Widget child) => ScaffoldWithNavBar( + // body: _AnimatedRouteBranchContainer(), + // )); // }, /// If it's necessary to customize the Page for StatefulShellRoute, /// provide a pageBuilder function in addition to the builder, for example: - // pageBuilder: (StatefulShellFactory shellFactory) { - // final Widget statefulShell = shellFactory.buildShell((BuildContext context, - // GoRouterState state, Widget child) => ScaffoldWithNavBar(body: child)); + // pageBuilder: (BuildContext context, GoRouterState state, ShellNavigatorBuilder navigatorBuilder) { + // final Widget statefulShell = navigatorBuilder.buildStatefulShell(context, state, + // (BuildContext context, GoRouterState state, Widget child) => ScaffoldWithNavBar(body: child)); // return NoTransitionPage(child: statefulShell); // }, ), diff --git a/packages/go_router/lib/go_router.dart b/packages/go_router/lib/go_router.dart index 0c759b305a9e..bc5431aa1ded 100644 --- a/packages/go_router/lib/go_router.dart +++ b/packages/go_router/lib/go_router.dart @@ -11,9 +11,11 @@ export 'src/configuration.dart' GoRoute, GoRouterState, RouteBase, + ShellNavigatorBuilder, ShellRoute, StatefulShellBranch, StatefulShellBranchState, + StatefulShellNavigationBuilder, StatefulShellRoute, StatefulShellRouteState; export 'src/misc/extensions.dart'; @@ -26,9 +28,8 @@ export 'src/typedefs.dart' GoRouterPageBuilder, GoRouterRedirect, GoRouterWidgetBuilder, - StatefulShellFactory, ShellRouteBuilder, ShellRoutePageBuilder, ShellBodyWidgetBuilder, - StatefulShellRouteBuilder, - StatefulShellRoutePageBuilder; + ShellRouteNavigationBuilder, + ShellRouteNavigationPageBuilder; diff --git a/packages/go_router/lib/src/builder.dart b/packages/go_router/lib/src/builder.dart index 064c61c568d0..23c83711eea3 100644 --- a/packages/go_router/lib/src/builder.dart +++ b/packages/go_router/lib/src/builder.dart @@ -5,15 +5,15 @@ import 'package:collection/collection.dart'; import 'package:flutter/widgets.dart'; +import '../go_router.dart'; import 'configuration.dart'; import 'delegate.dart'; import 'logging.dart'; import 'match.dart'; import 'matching.dart'; import 'misc/error_screen.dart'; -import 'misc/stateful_navigation_shell.dart'; +import 'navigator_builder.dart'; import 'pages/cupertino.dart'; -import 'pages/custom_transition_page.dart'; import 'pages/material.dart'; import 'route_data.dart'; import 'typedefs.dart'; @@ -91,8 +91,8 @@ class RouteBuilder { return GoRouterStateRegistryScope( registry: _registry, child: result); } on _RouteBuilderError catch (e) { - return _buildErrorNavigator(context, e, matchList.uri, onPopPage, - configuration.navigatorKey); + return _buildErrorNavigator( + context, e, matchList, onPopPage, configuration.navigatorKey); } }, ), @@ -114,7 +114,7 @@ class RouteBuilder { ) { return builderWithNav( context, - _buildNavigator( + RouteNavigatorBuilder.buildNavigator( onPopPage, buildPages(context, matchList, 0, onPopPage, routerNeglect, navigatorKey, registry), @@ -138,8 +138,8 @@ class RouteBuilder { try { final Map, List>> keyToPage = , List>>{}; - _buildRecursive(context, matchList, startIndex, onPopPage, routerNeglect, - keyToPage, navigatorKey, registry); + _buildRecursive(context, matchList.unmodifiableMatchList(), startIndex, + onPopPage, routerNeglect, keyToPage, navigatorKey, registry); // Every Page should have a corresponding RouteMatch. assert(keyToPage.values.flattened @@ -147,14 +147,43 @@ class RouteBuilder { return keyToPage[navigatorKey]!; } on _RouteBuilderError catch (e) { return >[ - _buildErrorPage(context, e, matchList.uri), + _buildErrorPage(context, e, matchList), ]; } } + /// Builds the pages for the given [RouteMatchList]. + Widget buildPreloadedNestedNavigator( + BuildContext context, + RouteMatchList matchList, + int startIndex, + PopPageCallback onPopPage, + bool routerNeglect, + GlobalKey navigatorKey, + {List observers = const [], + String? restorationScopeId}) { + try { + final Map, GoRouterState> newRegistry = + , GoRouterState>{}; + final Widget result = RouteNavigatorBuilder.buildNavigator( + onPopPage, + buildPages(context, matchList, startIndex, onPopPage, routerNeglect, + navigatorKey, newRegistry), + navigatorKey, + observers: observers, + restorationScopeId: restorationScopeId, + ); + _registry.updateRegistry(newRegistry, replace: false); + return result; + } on _RouteBuilderError catch (e) { + return _buildErrorNavigator( + context, e, matchList, onPopPage, configuration.navigatorKey); + } + } + void _buildRecursive( BuildContext context, - RouteMatchList matchList, + UnmodifiableRouteMatchList matchList, int startIndex, PopPageCallback onPopPage, bool routerNeglect, @@ -214,25 +243,10 @@ class RouteBuilder { _buildRecursive(context, matchList, startIndex + 1, onPopPage, routerNeglect, keyToPages, shellNavigatorKey, registry); - // Build the Navigator and/or StatefulNavigationShell - Navigator buildNavigator(String? restorationScopeId) => _buildNavigator( - onPopPage, keyToPages[shellNavigatorKey]!, shellNavigatorKey, - restorationScopeId: restorationScopeId); - final Widget child; - if (route is StatefulShellRoute) { - final String? restorationScopeId = - route.branchForSubRoute(subRoute).restorationScopeId; - child = _buildStatefulNavigationShell( - route, buildNavigator(restorationScopeId), state, matchList); - } else { - final String? restorationScopeId = - (route is ShellRoute) ? route.restorationScopeId : null; - child = buildNavigator(restorationScopeId); - } - // Build the Page for this route - final Page page = - _buildPageForRoute(context, state, match, child: child); + final Page page = _buildPageForRoute(context, state, match, + child: RouteNavigatorBuilder(this, route, shellNavigatorKey, + keyToPages[shellNavigatorKey]!, onPopPage)); registry[page] = state; // Place the ShellRoute's Page onto the list for the parent navigator. keyToPages @@ -241,65 +255,11 @@ class RouteBuilder { } } - Navigator _buildNavigator( - PopPageCallback onPopPage, - List> pages, - Key? navigatorKey, { - List observers = const [], - String? restorationScopeId, - }) { - return Navigator( - key: navigatorKey, - restorationScopeId: restorationScopeId ?? this.restorationScopeId, - pages: pages, - observers: observers, - onPopPage: onPopPage, - ); - } - - StatefulNavigationShell _buildStatefulNavigationShell( - StatefulShellRoute shellRoute, - Navigator navigator, - GoRouterState shellRouterState, - RouteMatchList currentMatchList, - ) { - return StatefulNavigationShell( - configuration: configuration, - shellRoute: shellRoute, - shellGoRouterState: shellRouterState, - shellBodyWidgetBuilder: (BuildContext _, GoRouterState __, Widget ___) => - throw _RouteBuilderError('ShellWidgetBuilder not configured'), - currentNavigator: navigator, - currentMatchList: currentMatchList.unmodifiableRouteMatchList(), - branchPreloadNavigatorBuilder: _preloadShellBranchNavigator, - ); - } - - Navigator _preloadShellBranchNavigator( - BuildContext context, - RouteMatchList matchList, - int startIndex, - GlobalKey navigatorKey, - PopPageCallback onPopPage, - String? restorationScopeId, - ) { - final Map, List>> keyToPage = - , List>>{}; - _buildRecursive(context, matchList, startIndex, onPopPage, true, keyToPage, - navigatorKey, , GoRouterState>{}); - - return _buildNavigator( - onPopPage, - keyToPage[navigatorKey]!, - navigatorKey, - restorationScopeId: restorationScopeId, - ); - } - /// Helper method that builds a [GoRouterState] object for the given [match] /// and [params]. @visibleForTesting - GoRouterState buildState(RouteMatchList matchList, RouteMatch match) { + GoRouterState buildState( + UnmodifiableRouteMatchList matchList, RouteMatch match) { final RouteBase route = match.route; String? name; String path = ''; @@ -309,8 +269,9 @@ class RouteBuilder { } final RouteMatchList effectiveMatchList = match is ImperativeRouteMatch ? match.matches : matchList; - return GoRouterState( + final GoRouterState state = GoRouterState( configuration, + matchList, location: effectiveMatchList.uri.toString(), subloc: match.subloc, name: name, @@ -323,12 +284,13 @@ class RouteBuilder { extra: match.extra, pageKey: match.pageKey, ); + return state; } /// Builds a [Page] for [StackedRoute] Page _buildPageForRoute( BuildContext context, GoRouterState state, RouteMatch match, - {Widget? child}) { + {Object? child}) { final RouteBase route = match.route; Page? page; @@ -339,10 +301,11 @@ class RouteBuilder { page = pageBuilder(context, state); } } else if (route is ShellRouteBase) { - final ShellRoutePageBuilder? pageBuilder = route.pageBuilder; - assert(child != null, '${route.runtimeType} must contain a child route'); + final ShellRouteNavigationPageBuilder? pageBuilder = route.pageBuilder; + assert(child is ShellNavigatorBuilder, + '${route.runtimeType} must contain a child route'); if (pageBuilder != null) { - page = pageBuilder(context, state, child!); + page = pageBuilder(context, state, child! as ShellNavigatorBuilder); } } @@ -351,7 +314,7 @@ class RouteBuilder { } page ??= buildPage(context, state, Builder(builder: (BuildContext context) { - return _callRouteBuilder(context, state, match, childWidget: child); + return _callRouteBuilder(context, state, match, child: child); })); _setRouteMatchForPage(page, match); @@ -362,7 +325,7 @@ class RouteBuilder { /// Calls the user-provided route builder from the [RouteMatch]'s [RouteBase]. Widget _callRouteBuilder( BuildContext context, GoRouterState state, RouteMatch match, - {Widget? childWidget}) { + {Object? child}) { final RouteBase route = match.route; if (route == null) { @@ -378,18 +341,18 @@ class RouteBuilder { return builder(context, state); } else if (route is ShellRouteBase) { - if (childWidget == null) { + if (child is! ShellNavigatorBuilder) { throw _RouteBuilderException( 'Attempt to build ShellRoute without a child widget'); } - final ShellRouteBuilder? builder = route.builder; + final ShellRouteNavigationBuilder? builder = route.builder; if (builder == null) { throw _RouteBuilderError('No builder provided to ShellRoute: $route'); } - return builder(context, state, childWidget); + return builder(context, state, child); } throw _RouteBuilderException('Unsupported route type $route'); @@ -470,13 +433,13 @@ class RouteBuilder { Widget _buildErrorNavigator( BuildContext context, _RouteBuilderError e, - Uri uri, + RouteMatchList matchList, PopPageCallback onPopPage, GlobalKey navigatorKey) { - return _buildNavigator( + return RouteNavigatorBuilder.buildNavigator( onPopPage, >[ - _buildErrorPage(context, e, uri), + _buildErrorPage(context, e, matchList), ], navigatorKey, ); @@ -486,10 +449,12 @@ class RouteBuilder { Page _buildErrorPage( BuildContext context, _RouteBuilderError error, - Uri uri, + RouteMatchList matchList, ) { + final Uri uri = matchList.uri; final GoRouterState state = GoRouterState( configuration, + matchList.unmodifiableMatchList(), location: uri.toString(), subloc: uri.path, name: null, diff --git a/packages/go_router/lib/src/delegate.dart b/packages/go_router/lib/src/delegate.dart index 2f1df55d8487..d84d1ff5e3a2 100644 --- a/packages/go_router/lib/src/delegate.dart +++ b/packages/go_router/lib/src/delegate.dart @@ -39,7 +39,7 @@ class GoRouterDelegate extends RouterDelegate /// Builds the top-level Navigator given a configuration and location. @visibleForTesting - final RouteBuilder builder; + late final RouteBuilder builder; /// Set to true to disable creating history entries on the web. final bool routerNeglect; diff --git a/packages/go_router/lib/src/matching.dart b/packages/go_router/lib/src/matching.dart index 93bfe4bc1296..d8f19dfcf66d 100644 --- a/packages/go_router/lib/src/matching.dart +++ b/packages/go_router/lib/src/matching.dart @@ -2,6 +2,7 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'package:collection/collection.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/widgets.dart'; @@ -55,7 +56,7 @@ class RouteMatchList { fullpath = _generateFullPath(matches); /// Creates an immutable clone of this RouteMatchList. - UnmodifiableRouteMatchList unmodifiableRouteMatchList() { + UnmodifiableRouteMatchList unmodifiableMatchList() { return UnmodifiableRouteMatchList.from(this); } @@ -139,33 +140,66 @@ class RouteMatchList { bool get isError => matches.length == 1 && matches.first.error != null; /// Returns the error that this match intends to display. - Exception? get error => matches.first.error; + Exception? get error => matches.firstOrNull?.error; } /// Unmodifiable version of [RouteMatchList]. @immutable -class UnmodifiableRouteMatchList { +class UnmodifiableRouteMatchList implements RouteMatchList { /// UnmodifiableRouteMatchList constructor. UnmodifiableRouteMatchList.from(RouteMatchList routeMatchList) : _matches = List.unmodifiable(routeMatchList.matches), _uri = routeMatchList.uri, - _pathParameters = - Map.unmodifiable(routeMatchList.pathParameters); + fullpath = routeMatchList.fullpath, + pathParameters = + Map.unmodifiable(routeMatchList.pathParameters), + error = routeMatchList.error, + extra = routeMatchList.extra, + isEmpty = routeMatchList.isEmpty, + isError = routeMatchList.isError, + isNotEmpty = routeMatchList.isNotEmpty; /// Creates a new [RouteMatchList] from this UnmodifiableRouteMatchList. - RouteMatchList get routeMatchList => RouteMatchList( + RouteMatchList get modifiableMatchList => RouteMatchList( List.from(_matches), _uri, - Map.from(_pathParameters)); + Map.from(pathParameters)); - /// The route matches. + @override + Uri get uri => _uri; + @override + final Uri _uri; + @override + set _uri(Uri uri) => throw UnimplementedError(); + + @override final List _matches; + @override + List get matches => _matches; - /// The uri of the current match. - final Uri _uri; + @override + final String fullpath; - /// Parameters for the matched route, URI-encoded. - final Map _pathParameters; + @override + final Map pathParameters; + + @override + final Exception? error; + + @override + final Object? extra; + + @override + final bool isEmpty; + + @override + final bool isError; + + @override + final bool isNotEmpty; + + @override + RouteMatch get last => _matches.last; @override bool operator ==(Object other) { @@ -177,11 +211,20 @@ class UnmodifiableRouteMatchList { } return listEquals(other._matches, _matches) && other._uri == _uri && - mapEquals(other._pathParameters, _pathParameters); + mapEquals(other.pathParameters, pathParameters); } @override - int get hashCode => Object.hash(_matches, _uri, _pathParameters); + int get hashCode => Object.hash(_matches, _uri, pathParameters); + + @override + void push(RouteMatch match) => throw UnimplementedError(); + + @override + void remove(RouteMatch match) => throw UnimplementedError(); + + @override + UnmodifiableRouteMatchList unmodifiableMatchList() => this; } /// An error that occurred during matching. diff --git a/packages/go_router/lib/src/misc/stateful_navigation_shell.dart b/packages/go_router/lib/src/misc/stateful_navigation_shell.dart index 9bf41040505b..f8ba6673bfd9 100644 --- a/packages/go_router/lib/src/misc/stateful_navigation_shell.dart +++ b/packages/go_router/lib/src/misc/stateful_navigation_shell.dart @@ -5,12 +5,7 @@ import 'package:collection/collection.dart'; import 'package:flutter/widgets.dart'; -import '../configuration.dart'; -import '../match.dart'; -import '../matching.dart'; -import '../parser.dart'; -import '../router.dart'; -import '../typedefs.dart'; +import '../../go_router.dart'; /// [InheritedWidget] for providing a reference to the closest /// [StatefulNavigationShellState]. @@ -32,16 +27,6 @@ class InheritedStatefulNavigationShell extends InheritedWidget { } } -/// Builder function for preloading a route branch navigator. -typedef BranchNavigatorPreloadBuilder = Navigator Function( - BuildContext context, - RouteMatchList navigatorMatchList, - int startIndex, - GlobalKey navigatorKey, - PopPageCallback onPopPage, - String? restorationScopeId, -); - /// Widget that manages and maintains the state of a [StatefulShellRoute], /// including the [Navigator]s of the configured route branches. /// @@ -57,101 +42,67 @@ typedef BranchNavigatorPreloadBuilder = Navigator Function( /// However, implementors can choose to disregard this and use an alternate /// container around the branch navigators /// (see [StatefulShellRouteState.children]) instead. -class StatefulNavigationShell extends StatefulWidget - implements StatefulShellFactory { +class StatefulNavigationShell extends StatefulWidget { /// Constructs an [StatefulNavigationShell]. const StatefulNavigationShell({ - required this.configuration, required this.shellRoute, + required this.navigatorBuilder, required this.shellGoRouterState, required this.shellBodyWidgetBuilder, - required this.currentNavigator, - required this.currentMatchList, - required this.branchPreloadNavigatorBuilder, super.key, }); - /// The route configuration for the app. - final RouteConfiguration configuration; - /// The associated [StatefulShellRoute] final StatefulShellRoute shellRoute; + /// The shell navigator builder. + final ShellNavigatorBuilder navigatorBuilder; + /// The [GoRouterState] for the navigation shell. final GoRouterState shellGoRouterState; /// The shell body widget builder. final ShellBodyWidgetBuilder shellBodyWidgetBuilder; - /// The navigator for the currently active route branch - final Navigator currentNavigator; - - /// The RouteMatchList for the current location - final UnmodifiableRouteMatchList currentMatchList; - - /// Builder for route branch navigators (used for preloading). - final BranchNavigatorPreloadBuilder branchPreloadNavigatorBuilder; - @override State createState() => StatefulNavigationShellState(); - - @override - Widget buildShell(ShellBodyWidgetBuilder shellWidgetBuilder) { - return StatefulNavigationShell( - configuration: configuration, - shellRoute: shellRoute, - shellGoRouterState: shellGoRouterState, - shellBodyWidgetBuilder: shellWidgetBuilder, - currentNavigator: currentNavigator, - currentMatchList: currentMatchList, - branchPreloadNavigatorBuilder: branchPreloadNavigatorBuilder, - key: key, - ); - } } /// State for StatefulNavigationShell. class StatefulNavigationShellState extends State { - final Map _navigatorCache = {}; + final Map _navigatorCache = {}; late StatefulShellRouteState _routeState; List get _branches => widget.shellRoute.branches; - PopPageCallback get _onPopPage => widget.currentNavigator.onPopPage!; + GlobalKey get _currentNavigatorKey => + widget.navigatorBuilder.navigatorKeyForCurrentRoute; - Navigator? _navigatorForBranch(StatefulShellBranch branch) { + Widget? _navigatorForBranch(StatefulShellBranch branch) { return _navigatorCache[branch.navigatorKey]; } - void _setNavigatorForBranch( - StatefulShellBranch branch, Navigator? navigator) { + void _setNavigatorForBranch(StatefulShellBranch branch, Widget? navigator) { navigator != null ? _navigatorCache[branch.navigatorKey] = navigator : _navigatorCache.remove(branch.navigatorKey); } int _findCurrentIndex() { - final int index = _branches.indexWhere((StatefulShellBranch e) => - e.navigatorKey == widget.currentNavigator.key); + final int index = _branches.indexWhere( + (StatefulShellBranch e) => e.navigatorKey == _currentNavigatorKey); assert(index >= 0); return index; } - void _switchActiveBranch(StatefulShellBranchState branchState, - UnmodifiableRouteMatchList? unmodifiableRouteMatchList) { + void _switchActiveBranch( + StatefulShellBranchState branchState, bool resetLocation) { final GoRouter goRouter = GoRouter.of(context); - final RouteMatchList? matchList = - unmodifiableRouteMatchList?.routeMatchList; - if (matchList != null && matchList.isNotEmpty) { - goRouter.routeInformationParser - .processRedirection(matchList, context) - .then( - (RouteMatchList matchList) => - goRouter.routerDelegate.setNewRoutePath(matchList), - onError: (_) => - goRouter.go(_defaultBranchLocation(branchState.branch)), - ); + final GoRouterState? routeState = branchState.routeState; + if (routeState != null && !resetLocation) { + goRouter.goState(routeState, context).onError( + (_, __) => goRouter.go(_defaultBranchLocation(branchState.branch))); } else { goRouter.go(_defaultBranchLocation(branchState.branch)); } @@ -159,7 +110,9 @@ class StatefulNavigationShellState extends State { String _defaultBranchLocation(StatefulShellBranch branch) { return branch.defaultLocation ?? - widget.configuration.findStatefulShellBranchDefaultLocation(branch); + GoRouter.of(context) + .routeConfiguration + .findStatefulShellBranchDefaultLocation(branch); } void _preloadBranches() { @@ -178,43 +131,22 @@ class StatefulNavigationShellState extends State { Future _preloadBranch( StatefulShellBranchState branchState) { - // Parse a RouteMatchList from the default location of the route branch and - // handle any redirects - final GoRouteInformationParser parser = - GoRouter.of(context).routeInformationParser; - final Future routeMatchList = - parser.parseRouteInformationWithDependencies( - RouteInformation(location: _defaultBranchLocation(branchState.branch)), - context, + final Future navigatorBuilder = + widget.navigatorBuilder.buildPreloadedShellNavigator( + context: context, + location: _defaultBranchLocation(branchState.branch), + parentShellRoute: widget.shellRoute, + navigatorKey: branchState.navigatorKey, + // TODO, observers, + restorationScopeId: branchState.branch.restorationScopeId, ); - StatefulShellBranchState createBranchNavigator(RouteMatchList matchList) { - // Find the index of the branch root route in the match list - final StatefulShellBranch branch = branchState.branch; - final int shellRouteIndex = matchList.matches - .indexWhere((RouteMatch e) => e.route == widget.shellRoute); - // Keep only the routes from and below the root route in the match list and - // use that to build the Navigator for the branch - Navigator? navigator; - if (shellRouteIndex >= 0 && - shellRouteIndex < (matchList.matches.length - 1)) { - navigator = widget.branchPreloadNavigatorBuilder( - context, - matchList, - shellRouteIndex + 1, - branch.navigatorKey, - _onPopPage, - branch.restorationScopeId, - ); - } + return navigatorBuilder.then((Widget navigator) { return _updateStatefulShellBranchState( branchState, navigator: navigator, - matchList: matchList.unmodifiableRouteMatchList(), ); - } - - return routeMatchList.then(createBranchNavigator); + }); } void _updateRouteBranchState(StatefulShellBranchState branchState, @@ -251,6 +183,11 @@ class StatefulNavigationShellState extends State { final int index = _findCurrentIndex(); final StatefulShellBranch branch = _branches[index]; + // TODO: Observers + final Widget currentNavigator = widget.navigatorBuilder + .buildNavigatorForCurrentRoute( + restorationScopeId: branch.restorationScopeId); + // Update or create a new StatefulShellBranchState for the current branch // (i.e. the arguments currently provided to the Widget). StatefulShellBranchState? currentBranchState = _routeState.branchStates @@ -258,14 +195,14 @@ class StatefulNavigationShellState extends State { if (currentBranchState != null) { currentBranchState = _updateStatefulShellBranchState( currentBranchState, - navigator: widget.currentNavigator, - matchList: widget.currentMatchList, + navigator: currentNavigator, + routeState: widget.shellGoRouterState, ); } else { currentBranchState = _createStatefulShellBranchState( branch, - navigator: widget.currentNavigator, - matchList: widget.currentMatchList, + navigator: currentNavigator, + routeState: widget.shellGoRouterState, ); } @@ -299,13 +236,13 @@ class StatefulNavigationShellState extends State { StatefulShellBranchState _updateStatefulShellBranchState( StatefulShellBranchState branchState, { - Navigator? navigator, - UnmodifiableRouteMatchList? matchList, + Widget? navigator, + GoRouterState? routeState, bool? loaded, }) { bool dirty = false; - if (matchList != null) { - dirty = branchState.matchList != matchList; + if (routeState != null) { + dirty = branchState.routeState != routeState; } if (navigator != null) { @@ -329,7 +266,7 @@ class StatefulNavigationShellState extends State { branch: branchState.branch, navigatorForBranch: _navigatorForBranch), isLoaded: isLoaded, - matchList: matchList, + routeState: routeState, ); } else { return branchState; @@ -337,9 +274,10 @@ class StatefulNavigationShellState extends State { } StatefulShellBranchState _createStatefulShellBranchState( - StatefulShellBranch branch, - {Navigator? navigator, - UnmodifiableRouteMatchList? matchList}) { + StatefulShellBranch branch, { + Widget? navigator, + GoRouterState? routeState, + }) { if (navigator != null) { _setNavigatorForBranch(branch, navigator); } @@ -347,7 +285,7 @@ class StatefulNavigationShellState extends State { branch: branch, child: _BranchNavigatorProxy( branch: branch, navigatorForBranch: _navigatorForBranch), - matchList: matchList, + routeState: routeState, ); } @@ -402,7 +340,7 @@ class StatefulNavigationShellState extends State { } } -typedef _NavigatorForBranch = Navigator? Function(StatefulShellBranch); +typedef _NavigatorForBranch = Widget? Function(StatefulShellBranch); /// Widget that serves as the proxy for a branch Navigator Widget, which /// possibly hasn't been created yet. diff --git a/packages/go_router/lib/src/navigator_builder.dart b/packages/go_router/lib/src/navigator_builder.dart new file mode 100644 index 000000000000..563f3bfdd045 --- /dev/null +++ b/packages/go_router/lib/src/navigator_builder.dart @@ -0,0 +1,99 @@ +import 'package:flutter/cupertino.dart'; + +import '../go_router.dart'; +import 'builder.dart'; +import 'configuration.dart'; +import 'match.dart'; +import 'matching.dart'; +import 'parser.dart'; + +/// Provides support for building Navigators for routes. +class RouteNavigatorBuilder extends ShellNavigatorBuilder { + /// Constructs a NavigatorBuilder. + RouteNavigatorBuilder(this.routeBuilder, this.currentRoute, + this.navigatorKeyForCurrentRoute, this.pages, this.onPopPage); + + /// The route builder. + final RouteBuilder routeBuilder; + @override + final ShellRouteBase currentRoute; + @override + final GlobalKey navigatorKeyForCurrentRoute; + + /// The pages for the current route. + final List> pages; + + /// The callback for popping a page. + final PopPageCallback onPopPage; + + /// Builds a navigator. + static Navigator buildNavigator( + PopPageCallback onPopPage, + List> pages, + Key? navigatorKey, { + List observers = const [], + String? restorationScopeId, + }) { + return Navigator( + key: navigatorKey, + restorationScopeId: restorationScopeId, + pages: pages, + observers: observers, + onPopPage: onPopPage, + ); + } + + @override + Widget buildNavigatorForCurrentRoute({ + List observers = const [], + String? restorationScopeId, + GlobalKey? navigatorKey, + }) { + return buildNavigator( + onPopPage, + pages, + navigatorKey ?? navigatorKeyForCurrentRoute, + observers: observers, + restorationScopeId: restorationScopeId, + ); + } + + @override + Future buildPreloadedShellNavigator({ + required BuildContext context, + required String location, + Object? extra, + required GlobalKey navigatorKey, + required ShellRouteBase parentShellRoute, + List observers = const [], + String? restorationScopeId, + }) { + // Parse a RouteMatchList from location and handle any redirects + final GoRouteInformationParser parser = + GoRouter.of(context).routeInformationParser; + final Future routeMatchList = + parser.parseRouteInformationWithDependencies( + RouteInformation(location: location, state: extra), + context, + ); + + Widget buildNavigator(RouteMatchList matchList) { + // Find the index of fromRoute in the match list + final int parentShellRouteIndex = matchList.matches + .indexWhere((RouteMatch e) => e.route == parentShellRoute); + assert(parentShellRouteIndex >= 0); + + return routeBuilder.buildPreloadedNestedNavigator( + context, + matchList, + parentShellRouteIndex + 1, + onPopPage, + true, + navigatorKey, + restorationScopeId: restorationScopeId, + ); + } + + return routeMatchList.then(buildNavigator); + } +} diff --git a/packages/go_router/lib/src/redirection.dart b/packages/go_router/lib/src/redirection.dart index 3ebef5cf294b..ba2cd3c91fb1 100644 --- a/packages/go_router/lib/src/redirection.dart +++ b/packages/go_router/lib/src/redirection.dart @@ -92,6 +92,7 @@ FutureOr redirect( context, GoRouterState( configuration, + prevMatchList.unmodifiableMatchList(), location: prevLocation, name: null, // No name available at the top level trim the query params off the @@ -137,6 +138,7 @@ FutureOr _getRouteLevelRedirect( context, GoRouterState( configuration, + matchList.unmodifiableMatchList(), location: matchList.uri.toString(), subloc: match.subloc, name: route.name, diff --git a/packages/go_router/lib/src/route.dart b/packages/go_router/lib/src/route.dart index a28770ce2958..9659745e05e0 100644 --- a/packages/go_router/lib/src/route.dart +++ b/packages/go_router/lib/src/route.dart @@ -7,6 +7,7 @@ import 'package:flutter/widgets.dart'; import 'package:meta/meta.dart'; import 'configuration.dart'; +import 'misc/stateful_navigation_shell.dart'; import 'pages/custom_transition_page.dart'; import 'path_utils.dart'; import 'typedefs.dart'; @@ -324,8 +325,8 @@ class GoRoute extends RouteBase { /// Base class for classes that act as shells for sub-routes, such /// as [ShellRoute] and [StatefulShellRoute]. abstract class ShellRouteBase extends RouteBase { - const ShellRouteBase._({this.builder, this.pageBuilder, super.routes}) - : super._(); + /// Constructs a [ShellRouteBase]. + const ShellRouteBase({super.routes}) : super._(); /// The widget builder for a shell route. /// @@ -333,7 +334,7 @@ abstract class ShellRouteBase extends RouteBase { /// child parameter is the Widget managing the nested navigation for the /// matching sub-routes. Typically, a shell route builds its shell around this /// Widget. - final ShellRouteBuilder? builder; + ShellRouteNavigationBuilder? get builder; /// The page builder for a shell route. /// @@ -341,7 +342,7 @@ abstract class ShellRouteBase extends RouteBase { /// child parameter is the Widget managing the nested navigation for the /// matching sub-routes. Typically, a shell route builds its shell around this /// Widget. - final ShellRoutePageBuilder? pageBuilder; + ShellRouteNavigationPageBuilder? get pageBuilder; /// Returns the key for the [Navigator] that is to be used for the specified /// immediate sub-route of this shell route. @@ -446,14 +447,16 @@ abstract class ShellRouteBase extends RouteBase { class ShellRoute extends ShellRouteBase { /// Constructs a [ShellRoute]. ShellRoute({ - super.builder, - super.pageBuilder, + ShellRouteBuilder? builder, + ShellRoutePageBuilder? pageBuilder, super.routes, GlobalKey? navigatorKey, this.restorationScopeId, }) : assert(routes.isNotEmpty), navigatorKey = navigatorKey ?? GlobalKey(), - super._() { + _builder = builder, + _pageBuilder = pageBuilder, + super() { for (final RouteBase route in routes) { if (route is GoRoute) { assert(route.parentNavigatorKey == null || @@ -462,6 +465,36 @@ class ShellRoute extends ShellRouteBase { } } + final ShellRouteBuilder? _builder; + final ShellRoutePageBuilder? _pageBuilder; + + @override + late final ShellRouteNavigationBuilder? builder = () { + if (_builder == null) { + return null; + } + return (BuildContext context, GoRouterState state, + ShellNavigatorBuilder navigatorBuilder) { + // TODO: Observers + final Widget navigator = navigatorBuilder.buildNavigatorForCurrentRoute( + restorationScopeId: restorationScopeId); + return _builder!(context, state, navigator); + }; + }(); + + @override + late final ShellRouteNavigationPageBuilder? pageBuilder = () { + if (_pageBuilder == null) { + return null; + } + return (BuildContext context, GoRouterState state, + ShellNavigatorBuilder navigatorBuilder) { + final Widget navigator = navigatorBuilder.buildNavigatorForCurrentRoute( + restorationScopeId: restorationScopeId); + return _pageBuilder!(context, state, navigator); + }; + }(); + /// The [GlobalKey] to be used by the [Navigator] built for this route. /// All ShellRoutes build a Navigator by default. Child GoRoutes /// are placed onto this Navigator instead of the root Navigator. @@ -478,6 +511,33 @@ class ShellRoute extends ShellRouteBase { } } +/// Navigator builder for shell routes. +abstract class ShellNavigatorBuilder { + /// The [GlobalKey] to be used by the [Navigator] built for the current route. + GlobalKey get navigatorKeyForCurrentRoute; + + /// The current shell route. + ShellRouteBase get currentRoute; + + /// Builds a [Navigator] for the current route. + Widget buildNavigatorForCurrentRoute({ + List observers = const [], + String? restorationScopeId, + GlobalKey? navigatorKey, + }); + + /// Builds a preloaded [Navigator] for a specific location. + Future buildPreloadedShellNavigator({ + required BuildContext context, + required String location, + Object? extra, + required GlobalKey navigatorKey, + required ShellRouteBase parentShellRoute, + List observers = const [], + String? restorationScopeId, + }); +} + /// A route that displays a UI shell with separate [Navigator]s for its /// sub-routes. /// @@ -672,17 +732,14 @@ class StatefulShellRoute extends ShellRouteBase { /// in addition to the builder. StatefulShellRoute({ required this.branches, - StatefulShellRouteBuilder? builder, - StatefulShellRoutePageBuilder? pageBuilder, + this.builder, + this.pageBuilder, }) : assert(branches.isNotEmpty), assert((pageBuilder != null) ^ (builder != null), 'builder or pageBuilder must be provided'), assert(_debugUniqueNavigatorKeys(branches).length == branches.length, 'Navigator keys must be unique'), - super._( - builder: _builder(builder), - pageBuilder: _pageBuilder(pageBuilder), - routes: _routes(branches)) { + super(routes: _routes(branches)) { for (int i = 0; i < routes.length; ++i) { final RouteBase route = routes[i]; if (route is GoRoute) { @@ -692,26 +749,11 @@ class StatefulShellRoute extends ShellRouteBase { } } - static ShellRouteBuilder? _builder(StatefulShellRouteBuilder? builder) { - if (builder == null) { - return null; - } - return (BuildContext context, GoRouterState state, Widget child) { - assert(child is StatefulShellFactory); - return builder(child as StatefulShellFactory); - }; - } + @override + final ShellRouteNavigationBuilder? builder; - static ShellRoutePageBuilder? _pageBuilder( - StatefulShellRoutePageBuilder? builder) { - if (builder == null) { - return null; - } - return (BuildContext context, GoRouterState state, Widget child) { - assert(child is StatefulShellFactory); - return builder(child as StatefulShellFactory); - }; - } + @override + final ShellRouteNavigationPageBuilder? pageBuilder; /// Representations of the different stateful route branches that this /// shell route will manage. @@ -743,6 +785,22 @@ class StatefulShellRoute extends ShellRouteBase { branches.map((StatefulShellBranch e) => e.navigatorKey)); } +/// Extension on [ShellNavigatorBuilder] for building the Widget managing a +/// StatefulShellRoute. +extension StatefulShellNavigationBuilder on ShellNavigatorBuilder { + /// Builds the Widget managing a StatefulShellRoute. + Widget buildStatefulShell( + BuildContext context, GoRouterState state, ShellBodyWidgetBuilder body) { + assert(currentRoute is StatefulShellRoute); + return StatefulNavigationShell( + shellRoute: currentRoute as StatefulShellRoute, + navigatorBuilder: this, + shellGoRouterState: state, + shellBodyWidgetBuilder: body, + ); + } +} + /// Representation of a separate branch in a stateful navigation tree, used to /// configure [StatefulShellRoute]. /// diff --git a/packages/go_router/lib/src/router.dart b/packages/go_router/lib/src/router.dart index 73a86c645de6..aed73ab2aec6 100644 --- a/packages/go_router/lib/src/router.dart +++ b/packages/go_router/lib/src/router.dart @@ -132,8 +132,6 @@ class GoRouter extends ChangeNotifier implements RouterConfig { _routeInformationParser; /// The route configuration. Used for testing. - // TODO(johnpryan): Remove this, integration tests shouldn't need access - @visibleForTesting RouteConfiguration get routeConfiguration => _routeConfiguration; /// Gets the current location. @@ -202,6 +200,14 @@ class GoRouter extends ChangeNotifier implements RouterConfig { extra: extra, ); + /// Restore the location represented by the provided state. + Future goState(GoRouterState state, BuildContext context) { + final RouteMatchList matchList = state.routeMatchList.modifiableMatchList; + return routeInformationParser + .processRedirection(matchList, context) + .then(routerDelegate.setNewRoutePath); + } + /// Push a URI location onto the page stack w/ optional query parameters, e.g. /// `/family/f2/person/p1?color=blue` void push(String location, {Object? extra}) { diff --git a/packages/go_router/lib/src/shell_state.dart b/packages/go_router/lib/src/shell_state.dart index d78486e8cf25..013c82d806d7 100644 --- a/packages/go_router/lib/src/shell_state.dart +++ b/packages/go_router/lib/src/shell_state.dart @@ -7,7 +7,6 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/widgets.dart'; import '../go_router.dart'; -import 'matching.dart'; import 'misc/errors.dart'; import 'misc/stateful_navigation_shell.dart'; @@ -24,9 +23,7 @@ class StatefulShellRouteState { required this.route, required this.branchStates, required this.currentIndex, - required void Function( - StatefulShellBranchState, UnmodifiableRouteMatchList?) - switchActiveBranch, + required void Function(StatefulShellBranchState, bool) switchActiveBranch, required void Function(StatefulShellBranchState? branchState, bool navigateToDefaultLocation) resetState, @@ -66,8 +63,7 @@ class StatefulShellRouteState { GlobalKey get currentNavigatorKey => currentBranchState.branch.navigatorKey; - final void Function(StatefulShellBranchState, UnmodifiableRouteMatchList?) - _switchActiveBranch; + final void Function(StatefulShellBranchState, bool) _switchActiveBranch; final void Function( StatefulShellBranchState? branchState, bool navigateToDefaultLocation) @@ -130,13 +126,13 @@ class StatefulShellRouteState { name: name, index: index, ); - _switchActiveBranch(state, resetLocation ? null : state._matchList); + _switchActiveBranch(state, resetLocation); } /// Refreshes this StatefulShellRouteState by rebuilding the state for the /// current location. void refresh() { - _switchActiveBranch(currentBranchState, currentBranchState._matchList); + _switchActiveBranch(currentBranchState, true); } /// Resets this StatefulShellRouteState by clearing all navigation state of @@ -208,18 +204,18 @@ class StatefulShellBranchState { required this.branch, required this.child, this.isLoaded = false, - UnmodifiableRouteMatchList? matchList, - }) : _matchList = matchList; + this.routeState, + }); /// Constructs a copy of this [StatefulShellBranchState], with updated values for /// some of the fields. StatefulShellBranchState copy( - {Widget? child, bool? isLoaded, UnmodifiableRouteMatchList? matchList}) { + {Widget? child, bool? isLoaded, GoRouterState? routeState}) { return StatefulShellBranchState( branch: branch, child: child ?? this.child, isLoaded: isLoaded ?? this.isLoaded, - matchList: matchList ?? _matchList, + routeState: routeState ?? this.routeState, ); } @@ -235,8 +231,8 @@ class StatefulShellBranchState { /// the [StatefulShellRoute] is ignored (i.e. not added to the widget tree). final Widget child; - /// The current navigation stack for the branch. - final UnmodifiableRouteMatchList? _matchList; + /// The current GoRouterState associated with the branch. + final GoRouterState? routeState; /// Returns true if this branch has been loaded (i.e. visited once or /// pre-loaded). @@ -252,20 +248,14 @@ class StatefulShellBranchState { } return other.branch == branch && other.child == child && - other._matchList == _matchList; + other.routeState == routeState; } @override - int get hashCode => Object.hash(branch, child, _matchList); + int get hashCode => Object.hash(branch, child, routeState); /// Gets the state for the current branch of the nearest stateful shell route /// in the Widget tree. static StatefulShellBranchState of(BuildContext context) => StatefulShellRouteState.of(context).currentBranchState; } - -/// Helper extension on [StatefulShellBranchState], for internal use. -extension StatefulShellBranchStateHelper on StatefulShellBranchState { - /// The current navigation stack for the branch. - UnmodifiableRouteMatchList? get matchList => _matchList; -} diff --git a/packages/go_router/lib/src/state.dart b/packages/go_router/lib/src/state.dart index 721565a5e626..1234e0d7d17a 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 '../go_router.dart'; import 'configuration.dart'; +import 'matching.dart'; import 'misc/errors.dart'; /// The route state during routing. @@ -15,7 +16,8 @@ import 'misc/errors.dart'; class GoRouterState { /// Default constructor for creating route state during routing. const GoRouterState( - this._configuration, { + this._configuration, + this._routeMatchList, { required this.location, required this.subloc, required this.name, @@ -33,6 +35,13 @@ class GoRouterState { // See https://github.com/flutter/flutter/issues/107729 final RouteConfiguration _configuration; + /// Snapshot of the current route match list. + /// + /// Use to restore the navigation stack based on a GoRouterState, and also + /// to make two GoRouterState instances from different match lists unique + /// (i.e. not equal). + final UnmodifiableRouteMatchList _routeMatchList; + /// The full location of the route, e.g. /family/f2/person/p1 final String location; @@ -146,12 +155,24 @@ class GoRouterState { other.queryParametersAll == queryParametersAll && other.extra == extra && other.error == error && - other.pageKey == pageKey; + other.pageKey == pageKey && + other._routeMatchList == _routeMatchList; } @override - int get hashCode => Object.hash(location, subloc, name, path, fullpath, - params, queryParams, queryParametersAll, extra, error, pageKey); + int get hashCode => Object.hash( + location, + subloc, + name, + path, + fullpath, + params, + queryParams, + queryParametersAll, + extra, + error, + pageKey, + _routeMatchList); } /// An inherited widget to host a [GoRouterStateRegistry] for the subtree. @@ -214,7 +235,8 @@ class GoRouterStateRegistry extends ChangeNotifier { } /// Updates this registry with new records. - void updateRegistry(Map, GoRouterState> newRegistry) { + void updateRegistry(Map, GoRouterState> newRegistry, + {bool replace = true}) { bool shouldNotify = false; final Set> pagesWithAssociation = _routePageAssociation.values.toSet(); @@ -234,21 +256,31 @@ class GoRouterStateRegistry extends ChangeNotifier { // Adding or removing registry does not need to notify the listen since // no one should be depending on them. } - registry.removeWhere((Page key, GoRouterState value) { - if (newRegistry.containsKey(key)) { - return false; - } - // For those that have page route association, it will be removed by the - // route future. Need to notify the listener so they can update the page - // route association if its page has changed. - if (pagesWithAssociation.contains(key)) { - shouldNotify = true; - return false; - } - return true; - }); + if (replace) { + registry.removeWhere((Page key, GoRouterState value) { + if (newRegistry.containsKey(key)) { + return false; + } + // For those that have page route association, it will be removed by the + // route future. Need to notify the listener so they can update the page + // route association if its page has changed. + if (pagesWithAssociation.contains(key)) { + shouldNotify = true; + return false; + } + return true; + }); + } if (shouldNotify) { notifyListeners(); } } } + +/// Internal extension to expose the routeMatchList associated with a [GoRouterState]. +extension GoRouterStateInternal on GoRouterState { + /// The route match list associated with this [GoRouterState]. + UnmodifiableRouteMatchList get routeMatchList { + return _routeMatchList; + } +} diff --git a/packages/go_router/lib/src/typedefs.dart b/packages/go_router/lib/src/typedefs.dart index e818a22df9dc..403c5d068882 100644 --- a/packages/go_router/lib/src/typedefs.dart +++ b/packages/go_router/lib/src/typedefs.dart @@ -20,6 +20,20 @@ typedef GoRouterPageBuilder = Page Function( GoRouterState state, ); +/// The widget builder for [ShellRouteBase]. +typedef ShellRouteNavigationBuilder = Widget Function( + BuildContext context, + GoRouterState state, + ShellNavigatorBuilder navigatorBuilder, +); + +/// The page builder for [ShellRouteBase]. +typedef ShellRouteNavigationPageBuilder = Page Function( + BuildContext context, + GoRouterState state, + ShellNavigatorBuilder navigatorBuilder, +); + /// The widget builder for [ShellRoute]. typedef ShellRouteBuilder = Widget Function( BuildContext context, @@ -38,23 +52,6 @@ typedef ShellRoutePageBuilder = Page Function( typedef ShellBodyWidgetBuilder = Widget Function( BuildContext context, GoRouterState state, Widget child); -/// The factory for building the shell of a [StatefulShellRoute]. -abstract class StatefulShellFactory { - /// Builds the shell of a [StatefulShellRoute], using the provided builder to - /// build the body. - Widget buildShell(ShellBodyWidgetBuilder shellBodyWidgetBuilder); -} - -/// The widget builder for [StatefulShellRoute]. -typedef StatefulShellRouteBuilder = Widget Function( - StatefulShellFactory statefulNavigation, -); - -/// The page builder for [StatefulShellRoute]. -typedef StatefulShellRoutePageBuilder = Page Function( - StatefulShellFactory statefulNavigation, -); - /// The signature of the navigatorBuilder callback. typedef GoRouterNavigatorBuilder = Widget Function( BuildContext context, diff --git a/packages/go_router/test/builder_test.dart b/packages/go_router/test/builder_test.dart index 3ed1047289c1..d0a7800def43 100644 --- a/packages/go_router/test/builder_test.dart +++ b/packages/go_router/test/builder_test.dart @@ -9,7 +9,6 @@ import 'package:go_router/src/configuration.dart'; import 'package:go_router/src/match.dart'; import 'package:go_router/src/matching.dart'; import 'package:go_router/src/router.dart'; -import 'package:go_router/src/typedefs.dart'; import 'test_helpers.dart'; @@ -104,7 +103,7 @@ void main() { final RouteConfiguration config = RouteConfiguration( routes: [ StatefulShellRoute( - builder: (StatefulShellFactory factory) => factory.dummy(), + builder: mockStatefulShellBuilder, branches: [ StatefulShellBranch(navigatorKey: key, routes: [ GoRoute( @@ -156,7 +155,7 @@ void main() { const Text('Root'), routes: [ shell = StatefulShellRoute( - builder: (StatefulShellFactory factory) => factory.dummy(), + builder: mockStatefulShellBuilder, branches: [ StatefulShellBranch(navigatorKey: key, routes: [ nested = GoRoute( @@ -580,8 +579,11 @@ void main() { navigatorKey: rootNavigatorKey, routes: [ StatefulShellRoute( - builder: (StatefulShellFactory factory) { - return factory.buildShell( + builder: (BuildContext context, GoRouterState state, + ShellNavigatorBuilder navigatorBuilder) { + return navigatorBuilder.buildStatefulShell( + context, + state, (BuildContext context, GoRouterState state, Widget child) => _HomeScreen(child: child)); }, diff --git a/packages/go_router/test/configuration_test.dart b/packages/go_router/test/configuration_test.dart index 66f5d64d730b..e7721982a3fa 100644 --- a/packages/go_router/test/configuration_test.dart +++ b/packages/go_router/test/configuration_test.dart @@ -5,7 +5,8 @@ import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:go_router/src/configuration.dart'; -import 'package:go_router/src/typedefs.dart'; + +import 'test_helpers.dart'; void main() { group('RouteConfiguration', () { @@ -115,7 +116,7 @@ void main() { parentNavigatorKey: someNavigatorKey), ], ), - ], builder: _mockStatefulShellBuilder), + ], builder: mockStatefulShellBuilder), ], redirectLimit: 10, topRedirect: (BuildContext context, GoRouterState state) { @@ -145,7 +146,7 @@ void main() { routes: [ StatefulShellRoute(branches: [ StatefulShellBranch(routes: shellRouteChildren) - ], builder: _mockStatefulShellBuilder), + ], builder: mockStatefulShellBuilder), ], redirectLimit: 10, topRedirect: (BuildContext context, GoRouterState state) { @@ -186,7 +187,7 @@ void main() { StatefulShellBranch( routes: [routeB], navigatorKey: sectionBNavigatorKey), - ], builder: _mockStatefulShellBuilder), + ], builder: mockStatefulShellBuilder), ], redirectLimit: 10, topRedirect: (BuildContext context, GoRouterState state) { @@ -232,7 +233,7 @@ void main() { ), ], ), - ], builder: _mockStatefulShellBuilder), + ], builder: mockStatefulShellBuilder), ], redirectLimit: 10, topRedirect: (BuildContext context, GoRouterState state) { @@ -282,10 +283,10 @@ void main() { ), ], ), - ], builder: _mockStatefulShellBuilder), + ], builder: mockStatefulShellBuilder), ], ), - ], builder: _mockStatefulShellBuilder), + ], builder: mockStatefulShellBuilder), ], redirectLimit: 10, topRedirect: (BuildContext context, GoRouterState state) { @@ -365,7 +366,7 @@ void main() { ]), ], ), - ], builder: _mockStatefulShellBuilder), + ], builder: mockStatefulShellBuilder), ], ), StatefulShellBranch(routes: [ @@ -384,7 +385,7 @@ void main() { ], ), ]), - ], builder: _mockStatefulShellBuilder), + ], builder: mockStatefulShellBuilder), ], redirectLimit: 10, topRedirect: (BuildContext context, GoRouterState state) { @@ -404,7 +405,7 @@ void main() { navigatorKey: GlobalKey(debugLabel: 'root'), routes: [ StatefulShellRoute( - builder: _mockStatefulShellBuilder, + builder: mockStatefulShellBuilder, branches: [ branchA = StatefulShellBranch(routes: [ GoRoute( @@ -416,7 +417,7 @@ void main() { builder: _mockScreenBuilder, routes: [ StatefulShellRoute( - builder: _mockStatefulShellBuilder, + builder: mockStatefulShellBuilder, branches: [ branchY = StatefulShellBranch(routes: [ @@ -913,7 +914,3 @@ Widget _mockScreenBuilder(BuildContext context, GoRouterState state) => Widget _mockShellBuilder( BuildContext context, GoRouterState state, Widget child) => child; - -Widget _mockStatefulShellBuilder(StatefulShellFactory factory) => - factory.buildShell( - (BuildContext context, GoRouterState state, Widget child) => child); diff --git a/packages/go_router/test/delegate_test.dart b/packages/go_router/test/delegate_test.dart index fefe9ee3c6c6..270e709827e2 100644 --- a/packages/go_router/test/delegate_test.dart +++ b/packages/go_router/test/delegate_test.dart @@ -64,7 +64,7 @@ Future createGoRouterWithStatefulShellRoute( builder: (_, __) => const DummyStatefulWidget()), ]), ]), - ], builder: (StatefulShellFactory factory) => factory.dummy()), + ], builder: mockStatefulShellBuilder), ], ); await tester.pumpWidget(MaterialApp.router( diff --git a/packages/go_router/test/go_router_test.dart b/packages/go_router/test/go_router_test.dart index 16fe2ec50d2c..09ed2aaa4d89 100644 --- a/packages/go_router/test/go_router_test.dart +++ b/packages/go_router/test/go_router_test.dart @@ -2403,8 +2403,9 @@ void main() { StatefulShellRouteState? routeState; final List routes = [ StatefulShellRoute( - builder: (StatefulShellFactory factory) { - return factory.buildShell( + builder: (BuildContext context, GoRouterState state, + ShellNavigatorBuilder navigatorBuilder) { + return navigatorBuilder.buildStatefulShell(context, state, (BuildContext context, GoRouterState state, Widget child) { routeState = StatefulShellRouteState.of(context); return child; @@ -2927,8 +2928,9 @@ void main() { final List routes = [ StatefulShellRoute( - builder: (StatefulShellFactory factory) { - return factory.buildShell( + builder: (BuildContext context, GoRouterState state, + ShellNavigatorBuilder navigatorBuilder) { + return navigatorBuilder.buildStatefulShell(context, state, (BuildContext context, GoRouterState state, Widget child) { routeState = StatefulShellRouteState.of(context); return child; @@ -3038,8 +3040,9 @@ void main() { final List routes = [ StatefulShellRoute( - builder: (StatefulShellFactory factory) { - return factory.buildShell( + builder: (BuildContext context, GoRouterState state, + ShellNavigatorBuilder navigatorBuilder) { + return navigatorBuilder.buildStatefulShell(context, state, (BuildContext context, GoRouterState state, Widget child) { routeState = StatefulShellRouteState.of(context); return child; @@ -3113,8 +3116,9 @@ void main() { final List routes = [ StatefulShellRoute( - builder: (StatefulShellFactory factory) { - return factory.buildShell( + builder: (BuildContext context, GoRouterState state, + ShellNavigatorBuilder navigatorBuilder) { + return navigatorBuilder.buildStatefulShell(context, state, (BuildContext context, GoRouterState state, Widget child) { routeState = StatefulShellRouteState.of(context); return child; @@ -3200,8 +3204,9 @@ void main() { final List routes = [ StatefulShellRoute( - builder: (StatefulShellFactory factory) { - return factory.buildShell( + builder: (BuildContext context, GoRouterState state, + ShellNavigatorBuilder navigatorBuilder) { + return navigatorBuilder.buildStatefulShell(context, state, (BuildContext context, GoRouterState state, Widget child) { routeState = StatefulShellRouteState.of(context); return child; @@ -3261,8 +3266,9 @@ void main() { Text('Common - ${state.extra}'), ), StatefulShellRoute( - builder: (StatefulShellFactory factory) { - return factory.buildShell( + builder: (BuildContext context, GoRouterState state, + ShellNavigatorBuilder navigatorBuilder) { + return navigatorBuilder.buildStatefulShell(context, state, (BuildContext context, GoRouterState state, Widget child) { routeState = StatefulShellRouteState.of(context); return child; @@ -3317,6 +3323,16 @@ void main() { (WidgetTester tester) async { final GlobalKey rootNavigatorKey = GlobalKey(); + final GlobalKey b1NavigatorKey = + GlobalKey(debugLabel: 'b1'); + final GlobalKey b2NavigatorKey = + GlobalKey(debugLabel: 'b2'); + final GlobalKey b3NavigatorKey = + GlobalKey(debugLabel: 'b3'); + final GlobalKey b4NavigatorKey = + GlobalKey(debugLabel: 'b4'); + final GlobalKey b5NavigatorKey = + GlobalKey(debugLabel: 'b5'); final GlobalKey statefulWidgetKeyA = GlobalKey(); final GlobalKey statefulWidgetKeyB = @@ -3330,28 +3346,33 @@ void main() { final List routes = [ StatefulShellRoute( - builder: (StatefulShellFactory factory) => factory.dummy(), + builder: mockStatefulShellBuilder, 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), - ), - ]), + StatefulShellBranch( + navigatorKey: b1NavigatorKey, + routes: [ + GoRoute( + path: '/a', + builder: (BuildContext context, GoRouterState state) => + DummyStatefulWidget(key: statefulWidgetKeyA), + ), + ]), + StatefulShellBranch( + navigatorKey: b2NavigatorKey, + routes: [ + GoRoute( + path: '/b', + builder: (BuildContext context, GoRouterState state) => + DummyStatefulWidget(key: statefulWidgetKeyB), + ), + ]), ], ), StatefulShellRoute( - builder: (StatefulShellFactory factory) => factory.dummy(), + builder: mockStatefulShellBuilder, branches: [ StatefulShellBranch( + navigatorKey: b3NavigatorKey, preload: true, routes: [ GoRoute( @@ -3362,6 +3383,7 @@ void main() { ], ), StatefulShellBranch( + navigatorKey: b4NavigatorKey, preload: true, routes: [ GoRoute( @@ -3372,6 +3394,7 @@ void main() { ], ), StatefulShellBranch( + navigatorKey: b5NavigatorKey, preload: true, routes: [ GoRoute( @@ -3424,8 +3447,9 @@ void main() { final List routes = [ StatefulShellRoute( - builder: (StatefulShellFactory factory) { - return factory.buildShell( + builder: (BuildContext context, GoRouterState state, + ShellNavigatorBuilder navigatorBuilder) { + return navigatorBuilder.buildStatefulShell(context, state, (BuildContext context, GoRouterState state, Widget child) { routeState = StatefulShellRouteState.of(context); return child; @@ -3521,8 +3545,9 @@ void main() { final List routes = [ StatefulShellRoute( - builder: (StatefulShellFactory factory) { - return factory.buildShell( + builder: (BuildContext context, GoRouterState state, + ShellNavigatorBuilder navigatorBuilder) { + return navigatorBuilder.buildStatefulShell(context, state, (BuildContext context, GoRouterState state, Widget child) { routeState = StatefulShellRouteState.of(context); return child; @@ -3590,8 +3615,9 @@ void main() { final List routes = [ StatefulShellRoute( - builder: (StatefulShellFactory factory) { - return factory.buildShell( + builder: (BuildContext context, GoRouterState state, + ShellNavigatorBuilder navigatorBuilder) { + return navigatorBuilder.buildStatefulShell(context, state, (BuildContext context, GoRouterState state, Widget child) { routeState = StatefulShellRouteState.of(context); return child; @@ -3779,9 +3805,7 @@ void main() { initialLocation: '/a', routes: [ StatefulShellRoute( - builder: (StatefulShellFactory factory) { - return factory.dummy(); - }, + builder: mockStatefulShellBuilder, branches: [ StatefulShellBranch(routes: [ GoRoute( diff --git a/packages/go_router/test/match_test.dart b/packages/go_router/test/match_test.dart index cf9a10ac2b6e..73531227b046 100644 --- a/packages/go_router/test/match_test.dart +++ b/packages/go_router/test/match_test.dart @@ -160,10 +160,8 @@ void main() { const {}, ); - final UnmodifiableRouteMatchList list1 = - list.unmodifiableRouteMatchList(); - final UnmodifiableRouteMatchList list2 = - list.unmodifiableRouteMatchList(); + final UnmodifiableRouteMatchList list1 = list.unmodifiableMatchList(); + final UnmodifiableRouteMatchList list2 = list.unmodifiableMatchList(); expect(list1, equals(list2)); }); diff --git a/packages/go_router/test/test_helpers.dart b/packages/go_router/test/test_helpers.dart index 1fa95e38eb9d..1231a0b46098 100644 --- a/packages/go_router/test/test_helpers.dart +++ b/packages/go_router/test/test_helpers.dart @@ -243,7 +243,11 @@ Future simulateAndroidBackButton(WidgetTester tester) async { .handlePlatformMessage('flutter/navigation', message, (_) {}); } -extension StatefulShellFactoryHelper on StatefulShellFactory { - Widget dummy() => buildShell( - (BuildContext context, GoRouterState state, Widget child) => child); -} +ShellRouteNavigationBuilder mockStatefulShellBuilder = ( + BuildContext context, + GoRouterState state, + ShellNavigatorBuilder navigatorBuilder, +) { + return navigatorBuilder.buildStatefulShell( + context, state, (_, __, Widget child) => child); +}; From c18941f1ca5678a60eb01114e70eec83e4d2d57c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20Lo=CC=88fstrand?= Date: Thu, 16 Feb 2023 17:26:48 +0100 Subject: [PATCH 079/112] Added NavigatorObserver support to StatefulShellRoute. --- packages/go_router/lib/src/builder.dart | 12 +++-- packages/go_router/lib/src/matching.dart | 7 +-- .../src/misc/stateful_navigation_shell.dart | 11 ++--- .../go_router/lib/src/navigator_builder.dart | 35 +++++++++------ packages/go_router/lib/src/route.dart | 8 +++- packages/go_router/test/go_router_test.dart | 45 ++++++------------- 6 files changed, 61 insertions(+), 57 deletions(-) diff --git a/packages/go_router/lib/src/builder.dart b/packages/go_router/lib/src/builder.dart index 072ab5153be8..4351f383f616 100644 --- a/packages/go_router/lib/src/builder.dart +++ b/packages/go_router/lib/src/builder.dart @@ -143,8 +143,8 @@ class RouteBuilder { final Map, List>> keyToPage = , List>>{}; try { - _buildRecursive(context, matchList.unmodifiableMatchList(), 0, onPopPage, - routerNeglect, keyToPage, navigatorKey, registry); + _buildRecursive(context, matchList.unmodifiableMatchList(), startIndex, + onPopPage, routerNeglect, keyToPage, navigatorKey, registry); // Every Page should have a corresponding RouteMatch. assert(keyToPage.values.flattened @@ -161,7 +161,8 @@ class RouteBuilder { } } - /// Builds the pages for the given [RouteMatchList]. + /// Builds a preloaded nested [Navigator], containing a sub-tree (beginning + /// at startIndex) of the provided route match list. Widget buildPreloadedNestedNavigator( BuildContext context, RouteMatchList matchList, @@ -169,11 +170,13 @@ class RouteBuilder { PopPageCallback onPopPage, bool routerNeglect, GlobalKey navigatorKey, - {List observers = const [], + {List? observers, String? restorationScopeId}) { try { final Map, GoRouterState> newRegistry = , GoRouterState>{}; + final HeroController heroController = _goHeroCache.putIfAbsent( + navigatorKey, () => _getHeroController(context)); final Widget result = RouteNavigatorBuilder.buildNavigator( onPopPage, buildPages(context, matchList, startIndex, onPopPage, routerNeglect, @@ -181,6 +184,7 @@ class RouteBuilder { navigatorKey, observers: observers, restorationScopeId: restorationScopeId, + heroController: heroController, ); _registry.updateRegistry(newRegistry, replace: false); return result; diff --git a/packages/go_router/lib/src/matching.dart b/packages/go_router/lib/src/matching.dart index 21ba1a954136..147334e9736d 100644 --- a/packages/go_router/lib/src/matching.dart +++ b/packages/go_router/lib/src/matching.dart @@ -154,7 +154,7 @@ class UnmodifiableRouteMatchList implements RouteMatchList { /// UnmodifiableRouteMatchList constructor. UnmodifiableRouteMatchList.from(RouteMatchList routeMatchList) : _matches = List.unmodifiable(routeMatchList.matches), - _uri = routeMatchList.uri, + __uri = routeMatchList.uri, fullpath = routeMatchList.fullpath, pathParameters = Map.unmodifiable(routeMatchList.pathParameters), @@ -170,10 +170,11 @@ class UnmodifiableRouteMatchList implements RouteMatchList { _uri, Map.from(pathParameters)); + final Uri __uri; @override - Uri get uri => _uri; + Uri get uri => __uri; @override - final Uri _uri; + Uri get _uri => __uri; @override set _uri(Uri uri) => throw UnimplementedError(); diff --git a/packages/go_router/lib/src/misc/stateful_navigation_shell.dart b/packages/go_router/lib/src/misc/stateful_navigation_shell.dart index f8ba6673bfd9..51a93ea1ff1b 100644 --- a/packages/go_router/lib/src/misc/stateful_navigation_shell.dart +++ b/packages/go_router/lib/src/misc/stateful_navigation_shell.dart @@ -137,7 +137,7 @@ class StatefulNavigationShellState extends State { location: _defaultBranchLocation(branchState.branch), parentShellRoute: widget.shellRoute, navigatorKey: branchState.navigatorKey, - // TODO, observers, + observers: branchState.branch.observers, restorationScopeId: branchState.branch.restorationScopeId, ); @@ -183,10 +183,11 @@ class StatefulNavigationShellState extends State { final int index = _findCurrentIndex(); final StatefulShellBranch branch = _branches[index]; - // TODO: Observers - final Widget currentNavigator = widget.navigatorBuilder - .buildNavigatorForCurrentRoute( - restorationScopeId: branch.restorationScopeId); + final Widget currentNavigator = + widget.navigatorBuilder.buildNavigatorForCurrentRoute( + observers: branch.observers, + restorationScopeId: branch.restorationScopeId, + ); // Update or create a new StatefulShellBranchState for the current branch // (i.e. the arguments currently provided to the Widget). diff --git a/packages/go_router/lib/src/navigator_builder.dart b/packages/go_router/lib/src/navigator_builder.dart index 6dc727339934..dec8b9208671 100644 --- a/packages/go_router/lib/src/navigator_builder.dart +++ b/packages/go_router/lib/src/navigator_builder.dart @@ -24,6 +24,7 @@ class RouteNavigatorBuilder extends ShellNavigatorBuilder { @override final ShellRouteBase currentRoute; + /// The hero controller. final HeroController heroController; @override @@ -36,20 +37,29 @@ class RouteNavigatorBuilder extends ShellNavigatorBuilder { final PopPageCallback onPopPage; /// Builds a navigator. - static Navigator buildNavigator( + static Widget buildNavigator( PopPageCallback onPopPage, List> pages, Key? navigatorKey, { List? observers, String? restorationScopeId, + HeroController? heroController, }) { - return Navigator( + final Widget navigator = Navigator( key: navigatorKey, restorationScopeId: restorationScopeId, pages: pages, observers: observers ?? const [], onPopPage: onPopPage, ); + if (heroController != null) { + return HeroControllerScope( + controller: heroController, + child: navigator, + ); + } else { + return navigator; + } } @override @@ -58,15 +68,14 @@ class RouteNavigatorBuilder extends ShellNavigatorBuilder { String? restorationScopeId, GlobalKey? navigatorKey, }) { - return HeroControllerScope( - controller: heroController, - child: buildNavigator( - onPopPage, - pages, - navigatorKey ?? navigatorKeyForCurrentRoute, - observers: observers, - restorationScopeId: restorationScopeId, - )); + return buildNavigator( + onPopPage, + pages, + navigatorKey ?? navigatorKeyForCurrentRoute, + observers: observers, + restorationScopeId: restorationScopeId, + heroController: heroController, + ); } @override @@ -76,7 +85,7 @@ class RouteNavigatorBuilder extends ShellNavigatorBuilder { Object? extra, required GlobalKey navigatorKey, required ShellRouteBase parentShellRoute, - List observers = const [], + List? observers, String? restorationScopeId, }) { // Parse a RouteMatchList from location and handle any redirects @@ -93,7 +102,6 @@ class RouteNavigatorBuilder extends ShellNavigatorBuilder { final int parentShellRouteIndex = matchList.matches .indexWhere((RouteMatch e) => e.route == parentShellRoute); assert(parentShellRouteIndex >= 0); - return routeBuilder.buildPreloadedNestedNavigator( context, matchList, @@ -102,6 +110,7 @@ class RouteNavigatorBuilder extends ShellNavigatorBuilder { true, navigatorKey, restorationScopeId: restorationScopeId, + observers: observers, ); } diff --git a/packages/go_router/lib/src/route.dart b/packages/go_router/lib/src/route.dart index 4e9402aea6bb..4e2bad1ca840 100644 --- a/packages/go_router/lib/src/route.dart +++ b/packages/go_router/lib/src/route.dart @@ -539,7 +539,7 @@ abstract class ShellNavigatorBuilder { Object? extra, required GlobalKey navigatorKey, required ShellRouteBase parentShellRoute, - List observers = const [], + List? observers, String? restorationScopeId, }); } @@ -826,6 +826,7 @@ class StatefulShellBranch { this.defaultLocation, this.name, this.restorationScopeId, + this.observers, this.preload = false, }) : navigatorKey = navigatorKey ?? GlobalKey( @@ -869,6 +870,11 @@ class StatefulShellBranch { /// its history. final String? restorationScopeId; + /// The observers for this branch. + /// + /// The observers parameter is used by the [Navigator] built for this branch. + final List? observers; + @override bool operator ==(Object other) { if (identical(other, this)) { diff --git a/packages/go_router/test/go_router_test.dart b/packages/go_router/test/go_router_test.dart index 2d02f5078f7b..f4859dda32b7 100644 --- a/packages/go_router/test/go_router_test.dart +++ b/packages/go_router/test/go_router_test.dart @@ -3343,16 +3343,6 @@ void main() { (WidgetTester tester) async { final GlobalKey rootNavigatorKey = GlobalKey(); - final GlobalKey b1NavigatorKey = - GlobalKey(debugLabel: 'b1'); - final GlobalKey b2NavigatorKey = - GlobalKey(debugLabel: 'b2'); - final GlobalKey b3NavigatorKey = - GlobalKey(debugLabel: 'b3'); - final GlobalKey b4NavigatorKey = - GlobalKey(debugLabel: 'b4'); - final GlobalKey b5NavigatorKey = - GlobalKey(debugLabel: 'b5'); final GlobalKey statefulWidgetKeyA = GlobalKey(); final GlobalKey statefulWidgetKeyB = @@ -3368,31 +3358,26 @@ void main() { StatefulShellRoute( builder: mockStatefulShellBuilder, branches: [ - StatefulShellBranch( - navigatorKey: b1NavigatorKey, - routes: [ - GoRoute( - path: '/a', - builder: (BuildContext context, GoRouterState state) => - DummyStatefulWidget(key: statefulWidgetKeyA), - ), - ]), - StatefulShellBranch( - navigatorKey: b2NavigatorKey, - routes: [ - GoRoute( - path: '/b', - builder: (BuildContext context, GoRouterState state) => - DummyStatefulWidget(key: statefulWidgetKeyB), - ), - ]), + 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( builder: mockStatefulShellBuilder, branches: [ StatefulShellBranch( - navigatorKey: b3NavigatorKey, preload: true, routes: [ GoRoute( @@ -3403,7 +3388,6 @@ void main() { ], ), StatefulShellBranch( - navigatorKey: b4NavigatorKey, preload: true, routes: [ GoRoute( @@ -3414,7 +3398,6 @@ void main() { ], ), StatefulShellBranch( - navigatorKey: b5NavigatorKey, preload: true, routes: [ GoRoute( From b1ce76233dab2b9f93fc079c560e2f15e4fa2421 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20Lo=CC=88fstrand?= Date: Fri, 17 Feb 2023 00:12:50 +0100 Subject: [PATCH 080/112] Refactored the builder methods of the shell route classes to improve API ergonomics. Changed the way the branch child Widgets/Navigators are accessed, by adding the class ShellNavigatorContainer. --- .../example/lib/stateful_shell_route.dart | 107 ++---- packages/go_router/lib/go_router.dart | 7 +- packages/go_router/lib/src/builder.dart | 16 +- .../src/misc/stateful_navigation_shell.dart | 46 +-- .../go_router/lib/src/navigator_builder.dart | 4 + packages/go_router/lib/src/route.dart | 333 +++++++++--------- packages/go_router/lib/src/shell_state.dart | 31 +- packages/go_router/lib/src/typedefs.dart | 26 +- packages/go_router/test/builder_test.dart | 7 +- packages/go_router/test/go_router_test.dart | 45 +-- packages/go_router/test/test_helpers.dart | 9 +- 11 files changed, 281 insertions(+), 350 deletions(-) diff --git a/packages/go_router/example/lib/stateful_shell_route.dart b/packages/go_router/example/lib/stateful_shell_route.dart index 780310cf71ba..84ec1ec3b136 100644 --- a/packages/go_router/example/lib/stateful_shell_route.dart +++ b/packages/go_router/example/lib/stateful_shell_route.dart @@ -153,55 +153,42 @@ class NestedTabNavigationExampleApp extends StatelessWidget { ), ]), ], - builder: (BuildContext context, GoRouterState state, - ShellNavigatorBuilder navigatorBuilder) { - /// For this nested StatefulShellRoute we are using a custom - /// container (TabBarView) for the branch navigators, and thus - /// ignoring the default navigator contained passed to the - /// builder. Custom implementation can access the branch - /// navigators via the StatefulShellRouteState - /// (see TabbedRootScreen for details). - return navigatorBuilder.buildStatefulShell( - context, - state, - (BuildContext context, GoRouterState state, Widget child) => - const TabbedRootScreen(), + builder: (StatefulShellBuilder shellBuilder) { + /// For this nested StatefulShellRoute, a custom container + /// (TabBarView) is used for the branch navigators, and thus + /// ignoring the default navigator container passed to the + /// builder. Instead, the branch navigators are passed + /// directly to the TabbedRootScreen, using the children + /// field of ShellNavigatorContainer. See TabbedRootScreen + /// for more details on how the children are used in the + /// TabBarView. + return shellBuilder.buildShell( + (BuildContext context, GoRouterState state, + ShellNavigatorContainer child) => + TabbedRootScreen(children: child.children), ); }, ), ], ), ], - builder: (BuildContext context, GoRouterState state, - ShellNavigatorBuilder navigatorBuilder) { - return navigatorBuilder.buildStatefulShell( - context, - state, - (BuildContext context, GoRouterState state, Widget child) => - ScaffoldWithNavBar(body: child), - ); + builder: (StatefulShellBuilder shellBuilder) { + /// This builder implementation uses the default navigator container + /// (ShellNavigatorContainer) to host the branch navigators. This is + /// the simplest way to use StatefulShellRoute, when no separate + /// customization is needed for the branch Widgets (Navigators). + return shellBuilder.buildShell((BuildContext context, + GoRouterState state, ShellNavigatorContainer child) => + ScaffoldWithNavBar(body: child)); }, - /// It's possible to customize the container for the branch navigators - /// even further, to for instance setup custom animations. The code - /// below is an example of such a customization (see - /// _AnimatedRouteBranchContainer). Note that in this case, the Widget - /// provided in the child parameter should not be added to the widget - /// tree. Instead, access the child widgets of each branch directly - /// (see StatefulShellRouteState.children) to implement a custom layout - /// and container for the navigators. - // builder: (BuildContext context, GoRouterState state, ShellNavigatorBuilder navigatorBuilder) { - // return navigatorBuilder.buildStatefulShell(context, state, - // (BuildContext context, GoRouterState state, Widget child) => ScaffoldWithNavBar( - // body: _AnimatedRouteBranchContainer(), - // )); - // }, - /// If it's necessary to customize the Page for StatefulShellRoute, - /// provide a pageBuilder function in addition to the builder, for example: - // pageBuilder: (BuildContext context, GoRouterState state, ShellNavigatorBuilder navigatorBuilder) { - // final Widget statefulShell = navigatorBuilder.buildStatefulShell(context, state, - // (BuildContext context, GoRouterState state, Widget child) => ScaffoldWithNavBar(body: child)); + /// provide a pageBuilder function instead of the builder, for example: + // pageBuilder: (StatefulShellBuilder shellBuilder) { + // final Widget statefulShell = shellBuilder.buildShell( + // (BuildContext context, GoRouterState state, + // ShellNavigatorContainer child) => + // ScaffoldWithNavBar(body: child)); // return NoTransitionPage(child: statefulShell); // }, ), @@ -463,15 +450,15 @@ class ModalScreen extends StatelessWidget { /// Builds a nested shell using a [TabBar] and [TabBarView]. class TabbedRootScreen extends StatelessWidget { /// Constructs a TabbedRootScreen - const TabbedRootScreen({Key? key}) : super(key: key); + const TabbedRootScreen({required this.children, Key? key}) : super(key: key); - Widget _child(StatefulShellBranchState branchState) => branchState.child; + /// The children (Navigators) to display in the [TabBarView]. + final List children; @override Widget build(BuildContext context) { final StatefulShellRouteState shellState = StatefulShellRouteState.of(context); - final List children = shellState.branchStates.map(_child).toList(); final List tabs = children.mapIndexed((int i, _) => Tab(text: 'Tab ${i + 1}')).toList(); @@ -534,37 +521,3 @@ class TabScreen extends StatelessWidget { ); } } - -// ignore: unused_element -class _AnimatedRouteBranchContainer extends StatelessWidget { - @override - Widget build(BuildContext context) { - final StatefulShellRouteState shellRouteState = - StatefulShellRouteState.of(context); - final int currentIndex = shellRouteState.currentIndex; - - final PageController controller = PageController(initialPage: currentIndex); - return PageView( - controller: controller, - children: shellRouteState.children, - ); - - // // return Stack( - // // children: shellRouteState.children.mapIndexed( - // // (int index, Widget? navigator) { - // // return AnimatedScale( - // // scale: index == currentIndex ? 1 : 1.5, - // // duration: const Duration(milliseconds: 400), - // // child: AnimatedOpacity( - // // opacity: index == currentIndex ? 1 : 0, - // // duration: const Duration(milliseconds: 400), - // // child: Offstage( - // // offstage: index != currentIndex, - // // child: navigator ?? const SizedBox.shrink(), - // // ), - // // ), - // // ); - // // }, - // ).toList()); - } -} diff --git a/packages/go_router/lib/go_router.dart b/packages/go_router/lib/go_router.dart index bc5431aa1ded..920ad996c542 100644 --- a/packages/go_router/lib/go_router.dart +++ b/packages/go_router/lib/go_router.dart @@ -12,10 +12,11 @@ export 'src/configuration.dart' GoRouterState, RouteBase, ShellNavigatorBuilder, + ShellNavigatorContainer, ShellRoute, StatefulShellBranch, StatefulShellBranchState, - StatefulShellNavigationBuilder, + StatefulShellBuilder, StatefulShellRoute, StatefulShellRouteState; export 'src/misc/extensions.dart'; @@ -31,5 +32,5 @@ export 'src/typedefs.dart' ShellRouteBuilder, ShellRoutePageBuilder, ShellBodyWidgetBuilder, - ShellRouteNavigationBuilder, - ShellRouteNavigationPageBuilder; + StatefulShellRouteBuilder, + StatefulShellRoutePageBuilder; diff --git a/packages/go_router/lib/src/builder.dart b/packages/go_router/lib/src/builder.dart index 4351f383f616..da7e5253e7af 100644 --- a/packages/go_router/lib/src/builder.dart +++ b/packages/go_router/lib/src/builder.dart @@ -261,7 +261,7 @@ class RouteBuilder { // Build the Page for this route final Page page = _buildPageForRoute(context, state, match, - child: RouteNavigatorBuilder(this, route, heroController, + child: RouteNavigatorBuilder(this, state, route, heroController, shellNavigatorKey, keyToPages[shellNavigatorKey]!, onPopPage)); registry[page] = state; // Place the ShellRoute's Page onto the list for the parent navigator. @@ -317,12 +317,9 @@ class RouteBuilder { page = pageBuilder(context, state); } } else if (route is ShellRouteBase) { - final ShellRouteNavigationPageBuilder? pageBuilder = route.pageBuilder; assert(child is ShellNavigatorBuilder, - '${route.runtimeType} must contain a child route'); - if (pageBuilder != null) { - page = pageBuilder(context, state, child! as ShellNavigatorBuilder); - } + '${route.runtimeType} must contain a child'); + page = route.buildPage(context, state, child! as ShellNavigatorBuilder); } if (page is NoOpPage) { @@ -358,13 +355,12 @@ class RouteBuilder { 'Attempt to build ShellRoute without a child widget'); } - final ShellRouteNavigationBuilder? builder = route.builder; - - if (builder == null) { + final Widget? widget = route.buildWidget(context, state, child); + if (widget == null) { throw _RouteBuilderError('No builder provided to ShellRoute: $route'); } - return builder(context, state, child); + return widget; } throw _RouteBuilderException('Unsupported route type $route'); diff --git a/packages/go_router/lib/src/misc/stateful_navigation_shell.dart b/packages/go_router/lib/src/misc/stateful_navigation_shell.dart index 51a93ea1ff1b..8bea2cf504de 100644 --- a/packages/go_router/lib/src/misc/stateful_navigation_shell.dart +++ b/packages/go_router/lib/src/misc/stateful_navigation_shell.dart @@ -47,7 +47,6 @@ class StatefulNavigationShell extends StatefulWidget { const StatefulNavigationShell({ required this.shellRoute, required this.navigatorBuilder, - required this.shellGoRouterState, required this.shellBodyWidgetBuilder, super.key, }); @@ -58,9 +57,6 @@ class StatefulNavigationShell extends StatefulWidget { /// The shell navigator builder. final ShellNavigatorBuilder navigatorBuilder; - /// The [GoRouterState] for the navigation shell. - final GoRouterState shellGoRouterState; - /// The shell body widget builder. final ShellBodyWidgetBuilder shellBodyWidgetBuilder; @@ -76,6 +72,7 @@ class StatefulNavigationShellState extends State { List get _branches => widget.shellRoute.branches; + GoRouterState get _currentGoRouterState => widget.navigatorBuilder.state; GlobalKey get _currentNavigatorKey => widget.navigatorBuilder.navigatorKeyForCurrentRoute; @@ -197,13 +194,13 @@ class StatefulNavigationShellState extends State { currentBranchState = _updateStatefulShellBranchState( currentBranchState, navigator: currentNavigator, - routeState: widget.shellGoRouterState, + routeState: _currentGoRouterState, ); } else { currentBranchState = _createStatefulShellBranchState( branch, navigator: currentNavigator, - routeState: widget.shellGoRouterState, + routeState: _currentGoRouterState, ); } @@ -263,9 +260,6 @@ class StatefulNavigationShellState extends State { if (dirty) { return branchState.copy( - child: _BranchNavigatorProxy( - branch: branchState.branch, - navigatorForBranch: _navigatorForBranch), isLoaded: isLoaded, routeState: routeState, ); @@ -284,8 +278,6 @@ class StatefulNavigationShellState extends State { } return StatefulShellBranchState( branch: branch, - child: _BranchNavigatorProxy( - branch: branch, navigatorForBranch: _navigatorForBranch), routeState: routeState, ); } @@ -324,6 +316,11 @@ class StatefulNavigationShellState extends State { @override Widget build(BuildContext context) { + final List children = _branches + .map((StatefulShellBranch branch) => _BranchNavigatorProxy( + branch: branch, navigatorForBranch: _navigatorForBranch)) + .toList(); + return InheritedStatefulNavigationShell( routeState: _routeState, child: Builder(builder: (BuildContext context) { @@ -333,8 +330,9 @@ class StatefulNavigationShellState extends State { widget.shellBodyWidgetBuilder; return shellWidgetBuilder( context, - widget.shellGoRouterState, - _IndexedStackedRouteBranchContainer(routeState: _routeState), + _currentGoRouterState, + _IndexedStackedRouteBranchContainer( + routeState: _routeState, children: children), ); }), ); @@ -362,29 +360,33 @@ class _BranchNavigatorProxy extends StatelessWidget { /// Default implementation of a container widget for the [Navigator]s of the /// route branches. This implementation uses an [IndexedStack] as a container. -class _IndexedStackedRouteBranchContainer extends StatelessWidget { - const _IndexedStackedRouteBranchContainer({required this.routeState}); +class _IndexedStackedRouteBranchContainer extends ShellNavigatorContainer { + const _IndexedStackedRouteBranchContainer( + {required this.routeState, required this.children}); final StatefulShellRouteState routeState; + @override + final List children; + @override Widget build(BuildContext context) { final int currentIndex = routeState.currentIndex; - final List children = routeState.branchStates - .mapIndexed((int index, StatefulShellBranchState item) => - _buildRouteBranchContainer(context, currentIndex == index, item)) + final List stackItems = children + .mapIndexed((int index, Widget child) => + _buildRouteBranchContainer(context, currentIndex == index, child)) .toList(); - return IndexedStack(index: currentIndex, children: children); + return IndexedStack(index: currentIndex, children: stackItems); } - Widget _buildRouteBranchContainer(BuildContext context, bool isActive, - StatefulShellBranchState navigatorState) { + Widget _buildRouteBranchContainer( + BuildContext context, bool isActive, Widget child) { return Offstage( offstage: !isActive, child: TickerMode( enabled: isActive, - child: navigatorState.child, + child: child, ), ); } diff --git a/packages/go_router/lib/src/navigator_builder.dart b/packages/go_router/lib/src/navigator_builder.dart index dec8b9208671..e9010129f22b 100644 --- a/packages/go_router/lib/src/navigator_builder.dart +++ b/packages/go_router/lib/src/navigator_builder.dart @@ -12,6 +12,7 @@ class RouteNavigatorBuilder extends ShellNavigatorBuilder { /// Constructs a NavigatorBuilder. RouteNavigatorBuilder( this.routeBuilder, + this.state, this.currentRoute, this.heroController, this.navigatorKeyForCurrentRoute, @@ -21,6 +22,9 @@ class RouteNavigatorBuilder extends ShellNavigatorBuilder { /// The route builder. final RouteBuilder routeBuilder; + @override + final GoRouterState state; + @override final ShellRouteBase currentRoute; diff --git a/packages/go_router/lib/src/route.dart b/packages/go_router/lib/src/route.dart index 4e2bad1ca840..571ed0fa4e5f 100644 --- a/packages/go_router/lib/src/route.dart +++ b/packages/go_router/lib/src/route.dart @@ -326,29 +326,57 @@ class GoRoute extends RouteBase { /// as [ShellRoute] and [StatefulShellRoute]. abstract class ShellRouteBase extends RouteBase { /// Constructs a [ShellRouteBase]. - const ShellRouteBase({super.routes}) : super._(); + const ShellRouteBase._({super.routes}) : super._(); - /// The widget builder for a shell route. + /// Attempts to build the Widget representing this shell route. /// - /// 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. - ShellRouteNavigationBuilder? get builder; + /// 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, + ShellNavigatorBuilder navigatorBuilder); - /// The page builder for a shell route. + /// Attempts to build the Page representing this shell route. /// - /// 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. - ShellRouteNavigationPageBuilder? get pageBuilder; + /// 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, + ShellNavigatorBuilder navigatorBuilder); /// 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); } +/// Navigator builder for shell routes. +abstract class ShellNavigatorBuilder { + /// The [GlobalKey] to be used by the [Navigator] built for the current route. + GlobalKey get navigatorKeyForCurrentRoute; + + /// The current route state. + GoRouterState get state; + + /// The current shell route. + ShellRouteBase get currentRoute; + + /// Builds a [Navigator] for the current route. + Widget buildNavigatorForCurrentRoute({ + List? observers, + String? restorationScopeId, + GlobalKey? navigatorKey, + }); + + /// Builds a preloaded [Navigator] for a specific location. + Future buildPreloadedShellNavigator({ + required BuildContext context, + required String location, + Object? extra, + required GlobalKey navigatorKey, + required ShellRouteBase parentShellRoute, + List? observers, + String? restorationScopeId, + }); +} + /// A route that displays a UI shell around the matching child route. /// /// When a ShellRoute is added to the list of routes on GoRouter or GoRoute, a @@ -447,17 +475,15 @@ abstract class ShellRouteBase extends RouteBase { class ShellRoute extends ShellRouteBase { /// Constructs a [ShellRoute]. ShellRoute({ - ShellRouteBuilder? builder, - ShellRoutePageBuilder? pageBuilder, + this.builder, + this.pageBuilder, this.observers, super.routes, GlobalKey? navigatorKey, this.restorationScopeId, }) : assert(routes.isNotEmpty), navigatorKey = navigatorKey ?? GlobalKey(), - _builder = builder, - _pageBuilder = pageBuilder, - super() { + super._() { for (final RouteBase route in routes) { if (route is GoRoute) { assert(route.parentNavigatorKey == null || @@ -466,34 +492,43 @@ class ShellRoute extends ShellRouteBase { } } - final ShellRouteBuilder? _builder; - final ShellRoutePageBuilder? _pageBuilder; + /// The widget builder for a shell route. + /// + /// 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. + final ShellRouteBuilder? builder; + + /// The page builder for a shell route. + /// + /// 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. + final ShellRoutePageBuilder? pageBuilder; @override - late final ShellRouteNavigationBuilder? builder = () { - if (_builder == null) { - return null; - } - return (BuildContext context, GoRouterState state, - ShellNavigatorBuilder navigatorBuilder) { + Widget? buildWidget(BuildContext context, GoRouterState state, + ShellNavigatorBuilder navigatorBuilder) { + if (builder != null) { final Widget navigator = navigatorBuilder.buildNavigatorForCurrentRoute( restorationScopeId: restorationScopeId, observers: observers); - return _builder!(context, state, navigator); - }; - }(); + return builder!(context, state, navigator); + } + return null; + } @override - late final ShellRouteNavigationPageBuilder? pageBuilder = () { - if (_pageBuilder == null) { - return null; - } - return (BuildContext context, GoRouterState state, - ShellNavigatorBuilder navigatorBuilder) { + Page? buildPage(BuildContext context, GoRouterState state, + ShellNavigatorBuilder navigatorBuilder) { + if (pageBuilder != null) { final Widget navigator = navigatorBuilder.buildNavigatorForCurrentRoute( restorationScopeId: restorationScopeId, observers: observers); - return _pageBuilder!(context, state, navigator); - }; - }(); + return pageBuilder!(context, state, navigator); + } + return null; + } /// The observers for a shell route. /// @@ -517,33 +552,6 @@ class ShellRoute extends ShellRouteBase { } } -/// Navigator builder for shell routes. -abstract class ShellNavigatorBuilder { - /// The [GlobalKey] to be used by the [Navigator] built for the current route. - GlobalKey get navigatorKeyForCurrentRoute; - - /// The current shell route. - ShellRouteBase get currentRoute; - - /// Builds a [Navigator] for the current route. - Widget buildNavigatorForCurrentRoute({ - List? observers, - String? restorationScopeId, - GlobalKey? navigatorKey, - }); - - /// Builds a preloaded [Navigator] for a specific location. - Future buildPreloadedShellNavigator({ - required BuildContext context, - required String location, - Object? extra, - required GlobalKey navigatorKey, - required ShellRouteBase parentShellRoute, - List? observers, - String? restorationScopeId, - }); -} - /// A route that displays a UI shell with separate [Navigator]s for its /// sub-routes. /// @@ -560,21 +568,18 @@ abstract class ShellNavigatorBuilder { /// StatefulShellBranch provides the root routes and the Navigator key ([GlobalKey]) /// for the branch, as well as an optional default location. /// -/// Like [ShellRoute], a [builder] and [pageBuilder] can be provided when -/// creating a StatefulShellRoute. However, StatefulShellRoute differs in that -/// the builder is mandatory and the pageBuilder may optionally be used in -/// addition to the builder. The reason for this is that the builder function is -/// not used to generate the final Widget used to represent the -/// StatefulShellRoute. Instead, the returned Widget will only form part of the -/// stateful navigation shell for this route. This means that the role of the -/// [builder] is to provide part of the navigation shell, whereas the role of -/// [pageBuilder] is simply to customize the Page used for this route (see -/// example below). -/// -/// In the builder function, the child parameter is a Widget that contains - and is -/// responsible for managing - the Navigators for the different route branches -/// of this StatefulShellRoute. This widget is meant to be used as the body of a -/// custom shell implementation, for example as the body of [Scaffold] with a +/// Like [ShellRoute], either a [builder] or a [pageBuilder] must be provided +/// when creating a StatefulShellRoute. However, these builders differ in that +/// they both accept only a single [StatefulShellBuilder] parameter, used for +/// building the stateful shell for the route. The shell builder in turn accepts +/// a [ShellBodyWidgetBuilder] parameter, used for providing the actual body of +/// the shell. +/// +/// In the ShellBodyWidgetBuilder function, the child parameter +/// ([ShellNavigatorContainer]) is a Widget that contains - and is responsible +/// for managing - the Navigators for the different route branches +/// of this StatefulShellRoute. This widget is meant to be used as the body of +/// the actual shell implementation, for example as the body of [Scaffold] with a /// [BottomNavigationBar]. /// /// The state of a StatefulShellRoute is represented by @@ -594,16 +599,18 @@ abstract class ShellNavigatorBuilder { /// ``` /// /// Sometimes greater control is needed over the layout and animations of the -/// Widgets representing the branch Navigators. In such cases, the child -/// argument in the builder function can be ignored, and a custom implementation -/// can instead be built using the child widgets of the branches -/// (see [StatefulShellRouteState.children]) directly. For example: +/// Widgets representing the branch Navigators. In such cases, a custom +/// implementation can access the Widgets containing the branch Navigators +/// directly through the field [ShellNavigatorContainer.children]. For example: /// /// ``` -/// final StatefulShellRouteState shellState = StatefulShellRouteState.of(context); -/// final int currentIndex = shellState.currentIndex; -/// final List children = shellRouteState.children; -/// return MyCustomShell(currentIndex, children); +/// builder: (StatefulShellBuilder shellBuilder) { +/// return shellBuilder.buildShell( +/// (BuildContext context, GoRouterState state, +/// ShellNavigatorContainer child) => +/// TabbedRootScreen(children: child.children), +/// ); +/// } /// ``` /// /// Below is a simple example of how a router configuration with @@ -623,9 +630,10 @@ abstract class ShellNavigatorBuilder { /// initialLocation: '/a', /// routes: [ /// StatefulShellRoute( -/// builder: (BuildContext context, GoRouterState state, -/// Widget navigatorContainer) { -/// return ScaffoldWithNavBar(body: navigatorContainer); +/// builder: (StatefulShellBuilder shellBuilder) { +/// return shellBuilder.buildShell( +/// (BuildContext context, GoRouterState state, Widget child) => +/// ScaffoldWithNavBar(body: child)); /// }, /// branches: [ /// /// The first branch, i.e. tab 'A' @@ -672,51 +680,6 @@ abstract class ShellNavigatorBuilder { /// ); /// ``` /// -/// When the [Page] for this route needs to be customized, a pageBuilder needs -/// to be provided. Note that this page builder doesn't replace the builder -/// function, but instead receives the stateful shell built by -/// [StatefulShellRoute] (using the builder function) as input. In other words, -/// builder and pageBuilder must both be provided when using a custom page for -/// a StatefulShellRoute. For example: -/// -/// ``` -/// final GoRouter _router = GoRouter( -/// initialLocation: '/a', -/// routes: [ -/// StatefulShellRoute( -/// builder: (BuildContext context, GoRouterState state, -/// Widget navigationContainer) { -/// return ScaffoldWithNavBar(body: navigationContainer); -/// }, -/// pageBuilder: -/// (BuildContext context, GoRouterState state, Widget statefulShell) { -/// return NoTransitionPage(child: statefulShell); -/// }, -/// branches: [ -/// /// The first branch, i.e. root of tab 'A' -/// StatefulShellBranch(routes: [ -/// GoRoute( -/// parentNavigatorKey: _tabANavigatorKey, -/// path: '/a', -/// builder: (BuildContext context, GoRouterState state) => -/// const RootScreen(label: 'A', detailsPath: '/a/details'), -/// ), -/// ]), -/// /// The second branch, i.e. root of tab 'B' -/// StatefulShellBranch(routes: [ -/// GoRoute( -/// parentNavigatorKey: _tabBNavigatorKey, -/// path: '/b', -/// builder: (BuildContext context, GoRouterState state) => -/// const RootScreen(label: 'B', detailsPath: '/b/details/1'), -/// ), -/// ]), -/// ], -/// ), -/// ], -/// ); -/// ``` -/// /// To access the current state of this route, to for instance access the /// index of the current route branch - use the method /// [StatefulShellRouteState.of]. For example: @@ -725,7 +688,7 @@ abstract class ShellNavigatorBuilder { /// final StatefulShellRouteState shellState = StatefulShellRouteState.of(context); /// ``` /// -/// See [Stateful Nested Navigation](https://github.com/flutter/packages/blob/main/packages/go_router/example/lib/stateful_nested_navigation.dart) +/// See [Stateful Nested Navigation](https://github.com/flutter/packages/blob/main/packages/go_router/example/lib/stateful_shell_route.dart) /// for a complete runnable example using StatefulShellRoute. class StatefulShellRoute extends ShellRouteBase { /// Constructs a [StatefulShellRoute] from a list of [StatefulShellBranch]es, @@ -745,7 +708,7 @@ class StatefulShellRoute extends ShellRouteBase { 'builder or pageBuilder must be provided'), assert(_debugUniqueNavigatorKeys(branches).length == branches.length, 'Navigator keys must be unique'), - super(routes: _routes(branches)) { + super._(routes: _routes(branches)) { for (int i = 0; i < routes.length; ++i) { final RouteBase route = routes[i]; if (route is GoRoute) { @@ -755,11 +718,44 @@ class StatefulShellRoute extends ShellRouteBase { } } - @override - final ShellRouteNavigationBuilder? builder; + /// The widget builder for a stateful shell route. + /// + /// Similar to [GoRoute.builder], but this builder function accepts a single + /// [StatefulShellBuilder] parameter, used for building the stateful shell for + /// this route. The shell builder in turn accepts a [ShellBodyWidgetBuilder] + /// parameter, used for providing the actual body of the shell. + /// + /// Example: + /// ``` + /// StatefulShellRoute( + /// builder: (StatefulShellBuilder shellBuilder) { + /// return shellBuilder.buildShell( + /// (BuildContext context, GoRouterState state, Widget child) => + /// ScaffoldWithNavBar(body: child)); + /// }, + /// ) + /// ``` + final StatefulShellRouteBuilder? builder; - @override - final ShellRouteNavigationPageBuilder? pageBuilder; + /// The page builder for a stateful shell route. + /// + /// Similar to [GoRoute.pageBuilder], This builder function accepts a single + /// [StatefulShellBuilder] parameter, used for building the stateful shell for + /// this route. The shell builder in turn accepts a [ShellBodyWidgetBuilder] + /// parameter, used for providing the actual body of the shell. + /// + /// Example: + /// ``` + /// StatefulShellRoute( + /// pageBuilder: (StatefulShellBuilder shellBuilder) { + /// final Widget statefulShell = shellBuilder.buildShell( + /// (BuildContext context, GoRouterState state, Widget child) => + /// ScaffoldWithNavBar(body: child)); + /// return MaterialPage(child: statefulShell); + /// }, + /// ) + /// ``` + final StatefulShellRoutePageBuilder? pageBuilder; /// Representations of the different stateful route branches that this /// shell route will manage. @@ -769,17 +765,29 @@ class StatefulShellRoute extends ShellRouteBase { final List branches; @override - GlobalKey navigatorKeyForSubRoute(RouteBase subRoute) { - return branchForSubRoute(subRoute).navigatorKey; + Widget? buildWidget(BuildContext context, GoRouterState state, + ShellNavigatorBuilder navigatorBuilder) { + if (builder != null) { + return builder!(StatefulShellBuilder(this, navigatorBuilder)); + } + return null; } - /// Returns the StatefulShellBranch that is to be used for the specified - /// immediate sub-route of this shell route. - StatefulShellBranch branchForSubRoute(RouteBase subRoute) { + @override + Page? buildPage(BuildContext context, GoRouterState state, + ShellNavigatorBuilder navigatorBuilder) { + if (pageBuilder != null) { + return pageBuilder!(StatefulShellBuilder(this, navigatorBuilder)); + } + return null; + } + + @override + GlobalKey navigatorKeyForSubRoute(RouteBase subRoute) { final StatefulShellBranch? branch = branches.firstWhereOrNull( (StatefulShellBranch e) => e.routes.contains(subRoute)); assert(branch != null); - return branch!; + return branch!.navigatorKey; } static List _routes(List branches) => @@ -791,22 +799,33 @@ class StatefulShellRoute extends ShellRouteBase { branches.map((StatefulShellBranch e) => e.navigatorKey)); } -/// Extension on [ShellNavigatorBuilder] for building the Widget managing a -/// StatefulShellRoute. -extension StatefulShellNavigationBuilder on ShellNavigatorBuilder { +/// Builds the Widget managing a StatefulShellRoute. +class StatefulShellBuilder { + /// Constructs a [StatefulShellBuilder]. + StatefulShellBuilder(this._shellRoute, this._builder); + + final StatefulShellRoute _shellRoute; + final ShellNavigatorBuilder _builder; + /// Builds the Widget managing a StatefulShellRoute. - Widget buildStatefulShell( - BuildContext context, GoRouterState state, ShellBodyWidgetBuilder body) { - assert(currentRoute is StatefulShellRoute); + Widget buildShell(ShellBodyWidgetBuilder body) { return StatefulNavigationShell( - shellRoute: currentRoute as StatefulShellRoute, - navigatorBuilder: this, - shellGoRouterState: state, + shellRoute: _shellRoute, + navigatorBuilder: _builder, shellBodyWidgetBuilder: body, ); } } +/// Widget containing the Navigators for the branches in a [StatefulShellRoute]. +abstract class ShellNavigatorContainer extends StatelessWidget { + /// Constructs a [ShellNavigatorContainer]. + const ShellNavigatorContainer({super.key}); + + /// The children (i.e. Navigators) of this ShellNavigatorContainer. + List get children; +} + /// Representation of a separate branch in a stateful navigation tree, used to /// configure [StatefulShellRoute]. /// diff --git a/packages/go_router/lib/src/shell_state.dart b/packages/go_router/lib/src/shell_state.dart index 013c82d806d7..bd415368a216 100644 --- a/packages/go_router/lib/src/shell_state.dart +++ b/packages/go_router/lib/src/shell_state.dart @@ -69,17 +69,6 @@ class StatefulShellRouteState { StatefulShellBranchState? branchState, bool navigateToDefaultLocation) _resetState; - /// Gets the [Widget]s representing each of the shell branches. - /// - /// The Widget returned from this method contains the [Navigator]s of the - /// branches. Note that the Widgets returned by this method should only be - /// added to the widget tree if using a custom branch container Widget - /// implementation, where the child parameter in the [ShellRouteBuilder] of - /// the [StatefulShellRoute] is ignored (i.e. not added to the widget tree). - /// See [StatefulShellBranchState.child]. - List get children => - branchStates.map((StatefulShellBranchState e) => e.child).toList(); - StatefulShellBranchState _branchStateFor({ GlobalKey? navigatorKey, String? name, @@ -202,18 +191,15 @@ class StatefulShellBranchState { /// Constructs a [StatefulShellBranchState]. const StatefulShellBranchState({ required this.branch, - required this.child, this.isLoaded = false, this.routeState, }); /// Constructs a copy of this [StatefulShellBranchState], with updated values for /// some of the fields. - StatefulShellBranchState copy( - {Widget? child, bool? isLoaded, GoRouterState? routeState}) { + StatefulShellBranchState copy({bool? isLoaded, GoRouterState? routeState}) { return StatefulShellBranchState( branch: branch, - child: child ?? this.child, isLoaded: isLoaded ?? this.isLoaded, routeState: routeState ?? this.routeState, ); @@ -222,15 +208,6 @@ class StatefulShellBranchState { /// The associated [StatefulShellBranch] final StatefulShellBranch branch; - /// The [Widget] representing this route branch in a [StatefulShellRoute]. - /// - /// The Widget returned from this method contains the [Navigator] of the - /// branch. Note that the Widget returned by this method should only - /// be added to the widget tree if using a custom branch container Widget - /// implementation, where the child parameter in the [ShellRouteBuilder] of - /// the [StatefulShellRoute] is ignored (i.e. not added to the widget tree). - final Widget child; - /// The current GoRouterState associated with the branch. final GoRouterState? routeState; @@ -246,13 +223,11 @@ class StatefulShellBranchState { if (other is! StatefulShellBranchState) { return false; } - return other.branch == branch && - other.child == child && - other.routeState == routeState; + return other.branch == branch && other.routeState == routeState; } @override - int get hashCode => Object.hash(branch, child, routeState); + int get hashCode => Object.hash(branch, routeState); /// Gets the state for the current branch of the nearest stateful shell route /// in the Widget tree. diff --git a/packages/go_router/lib/src/typedefs.dart b/packages/go_router/lib/src/typedefs.dart index 403c5d068882..2705a6dc92fe 100644 --- a/packages/go_router/lib/src/typedefs.dart +++ b/packages/go_router/lib/src/typedefs.dart @@ -20,20 +20,6 @@ typedef GoRouterPageBuilder = Page Function( GoRouterState state, ); -/// The widget builder for [ShellRouteBase]. -typedef ShellRouteNavigationBuilder = Widget Function( - BuildContext context, - GoRouterState state, - ShellNavigatorBuilder navigatorBuilder, -); - -/// The page builder for [ShellRouteBase]. -typedef ShellRouteNavigationPageBuilder = Page Function( - BuildContext context, - GoRouterState state, - ShellNavigatorBuilder navigatorBuilder, -); - /// The widget builder for [ShellRoute]. typedef ShellRouteBuilder = Widget Function( BuildContext context, @@ -48,9 +34,19 @@ typedef ShellRoutePageBuilder = Page Function( Widget child, ); +/// The widget builder for [StatefulShellRoute]. +typedef StatefulShellRouteBuilder = Widget Function( + StatefulShellBuilder navigatorBuilder, +); + +/// The page builder for [StatefulShellRoute]. +typedef StatefulShellRoutePageBuilder = Page Function( + StatefulShellBuilder navigatorBuilder, +); + /// The shell body widget builder for [StatefulShellRoute]. typedef ShellBodyWidgetBuilder = Widget Function( - BuildContext context, GoRouterState state, Widget child); + BuildContext context, GoRouterState state, ShellNavigatorContainer child); /// The signature of the navigatorBuilder callback. typedef GoRouterNavigatorBuilder = Widget Function( diff --git a/packages/go_router/test/builder_test.dart b/packages/go_router/test/builder_test.dart index d0a7800def43..c82831e9cfe5 100644 --- a/packages/go_router/test/builder_test.dart +++ b/packages/go_router/test/builder_test.dart @@ -579,11 +579,8 @@ void main() { navigatorKey: rootNavigatorKey, routes: [ StatefulShellRoute( - builder: (BuildContext context, GoRouterState state, - ShellNavigatorBuilder navigatorBuilder) { - return navigatorBuilder.buildStatefulShell( - context, - state, + builder: (StatefulShellBuilder shellBuilder) { + return shellBuilder.buildShell( (BuildContext context, GoRouterState state, Widget child) => _HomeScreen(child: child)); }, diff --git a/packages/go_router/test/go_router_test.dart b/packages/go_router/test/go_router_test.dart index f4859dda32b7..28d07d0aef34 100644 --- a/packages/go_router/test/go_router_test.dart +++ b/packages/go_router/test/go_router_test.dart @@ -2408,9 +2408,8 @@ void main() { StatefulShellRouteState? routeState; final List routes = [ StatefulShellRoute( - builder: (BuildContext context, GoRouterState state, - ShellNavigatorBuilder navigatorBuilder) { - return navigatorBuilder.buildStatefulShell(context, state, + builder: (StatefulShellBuilder shellBuilder) { + return shellBuilder.buildShell( (BuildContext context, GoRouterState state, Widget child) { routeState = StatefulShellRouteState.of(context); return child; @@ -2948,9 +2947,8 @@ void main() { final List routes = [ StatefulShellRoute( - builder: (BuildContext context, GoRouterState state, - ShellNavigatorBuilder navigatorBuilder) { - return navigatorBuilder.buildStatefulShell(context, state, + builder: (StatefulShellBuilder shellBuilder) { + return shellBuilder.buildShell( (BuildContext context, GoRouterState state, Widget child) { routeState = StatefulShellRouteState.of(context); return child; @@ -3060,9 +3058,8 @@ void main() { final List routes = [ StatefulShellRoute( - builder: (BuildContext context, GoRouterState state, - ShellNavigatorBuilder navigatorBuilder) { - return navigatorBuilder.buildStatefulShell(context, state, + builder: (StatefulShellBuilder shellBuilder) { + return shellBuilder.buildShell( (BuildContext context, GoRouterState state, Widget child) { routeState = StatefulShellRouteState.of(context); return child; @@ -3136,9 +3133,8 @@ void main() { final List routes = [ StatefulShellRoute( - builder: (BuildContext context, GoRouterState state, - ShellNavigatorBuilder navigatorBuilder) { - return navigatorBuilder.buildStatefulShell(context, state, + builder: (StatefulShellBuilder shellBuilder) { + return shellBuilder.buildShell( (BuildContext context, GoRouterState state, Widget child) { routeState = StatefulShellRouteState.of(context); return child; @@ -3224,9 +3220,8 @@ void main() { final List routes = [ StatefulShellRoute( - builder: (BuildContext context, GoRouterState state, - ShellNavigatorBuilder navigatorBuilder) { - return navigatorBuilder.buildStatefulShell(context, state, + builder: (StatefulShellBuilder shellBuilder) { + return shellBuilder.buildShell( (BuildContext context, GoRouterState state, Widget child) { routeState = StatefulShellRouteState.of(context); return child; @@ -3286,9 +3281,8 @@ void main() { Text('Common - ${state.extra}'), ), StatefulShellRoute( - builder: (BuildContext context, GoRouterState state, - ShellNavigatorBuilder navigatorBuilder) { - return navigatorBuilder.buildStatefulShell(context, state, + builder: (StatefulShellBuilder shellBuilder) { + return shellBuilder.buildShell( (BuildContext context, GoRouterState state, Widget child) { routeState = StatefulShellRouteState.of(context); return child; @@ -3450,9 +3444,8 @@ void main() { final List routes = [ StatefulShellRoute( - builder: (BuildContext context, GoRouterState state, - ShellNavigatorBuilder navigatorBuilder) { - return navigatorBuilder.buildStatefulShell(context, state, + builder: (StatefulShellBuilder shellBuilder) { + return shellBuilder.buildShell( (BuildContext context, GoRouterState state, Widget child) { routeState = StatefulShellRouteState.of(context); return child; @@ -3548,9 +3541,8 @@ void main() { final List routes = [ StatefulShellRoute( - builder: (BuildContext context, GoRouterState state, - ShellNavigatorBuilder navigatorBuilder) { - return navigatorBuilder.buildStatefulShell(context, state, + builder: (StatefulShellBuilder shellBuilder) { + return shellBuilder.buildShell( (BuildContext context, GoRouterState state, Widget child) { routeState = StatefulShellRouteState.of(context); return child; @@ -3618,9 +3610,8 @@ void main() { final List routes = [ StatefulShellRoute( - builder: (BuildContext context, GoRouterState state, - ShellNavigatorBuilder navigatorBuilder) { - return navigatorBuilder.buildStatefulShell(context, state, + builder: (StatefulShellBuilder shellBuilder) { + return shellBuilder.buildShell( (BuildContext context, GoRouterState state, Widget child) { routeState = StatefulShellRouteState.of(context); return child; diff --git a/packages/go_router/test/test_helpers.dart b/packages/go_router/test/test_helpers.dart index 97d5259daf83..add586cc2527 100644 --- a/packages/go_router/test/test_helpers.dart +++ b/packages/go_router/test/test_helpers.dart @@ -245,11 +245,8 @@ Future simulateAndroidBackButton(WidgetTester tester) async { .handlePlatformMessage('flutter/navigation', message, (_) {}); } -ShellRouteNavigationBuilder mockStatefulShellBuilder = ( - BuildContext context, - GoRouterState state, - ShellNavigatorBuilder navigatorBuilder, +StatefulShellRouteBuilder mockStatefulShellBuilder = ( + StatefulShellBuilder shellBuilder, ) { - return navigatorBuilder.buildStatefulShell( - context, state, (_, __, Widget child) => child); + return shellBuilder.buildShell((_, __, Widget child) => child); }; From 75e43d0e6783c6a28247b2969e7c8cfa815b7665 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20Lo=CC=88fstrand?= Date: Fri, 17 Feb 2023 01:16:47 +0100 Subject: [PATCH 081/112] Various refactoring. Updated StatefulShellRouteState.goBranch to only accept index for identifying branch. --- .../example/lib/stateful_shell_route.dart | 6 +- packages/go_router/lib/src/configuration.dart | 8 +-- .../src/misc/stateful_navigation_shell.dart | 2 +- packages/go_router/lib/src/route.dart | 51 +++++++------- packages/go_router/lib/src/shell_state.dart | 66 ++++--------------- .../go_router/test/configuration_test.dart | 12 ++-- packages/go_router/test/go_router_test.dart | 26 +------- 7 files changed, 52 insertions(+), 119 deletions(-) diff --git a/packages/go_router/example/lib/stateful_shell_route.dart b/packages/go_router/example/lib/stateful_shell_route.dart index 84ec1ec3b136..3e56bd4f7965 100644 --- a/packages/go_router/example/lib/stateful_shell_route.dart +++ b/packages/go_router/example/lib/stateful_shell_route.dart @@ -44,7 +44,7 @@ class NestedTabNavigationExampleApp extends StatelessWidget { branches: [ /// The route branch for the first tab of the bottom navigation bar. StatefulShellBranch( - /// To enable preloading of the default locations of branches, pass + /// To enable preloading of the initial locations of branches, pass /// true for the parameter preload. // preload: true, navigatorKey: _tabANavigatorKey, @@ -103,7 +103,7 @@ class NestedTabNavigationExampleApp extends StatelessWidget { /// The route branch for the third tab of the bottom navigation bar. StatefulShellBranch( /// StatefulShellBranch will automatically use the first descendant - /// GoRoute as the default location of the branch. If another route + /// GoRoute as the initial location of the branch. If another route /// is desired, specify the location of it using the defaultLocation /// parameter. // defaultLocation: '/c2', @@ -500,7 +500,7 @@ class TabScreen extends StatelessWidget { 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 default location ('/c1') + /// that is the initial location ('/c1') debugPrint('Building TabScreen - $label'); return Center( diff --git a/packages/go_router/lib/src/configuration.dart b/packages/go_router/lib/src/configuration.dart index 409b63e36944..240c531298da 100644 --- a/packages/go_router/lib/src/configuration.dart +++ b/packages/go_router/lib/src/configuration.dart @@ -132,19 +132,19 @@ class RouteConfiguration { for (final RouteBase route in routes) { if (route is StatefulShellRoute) { for (final StatefulShellBranch branch in route.branches) { - if (branch.defaultLocation == null) { + if (branch.initialLocation == null) { // Recursively search for the first GoRoute descendant. Will // throw assertion error if not found. findStatefulShellBranchDefaultLocation(branch); } else { final RouteBase defaultLocationRoute = - matcher.findMatch(branch.defaultLocation!).last.route; + matcher.findMatch(branch.initialLocation!).last.route; final RouteBase? match = branch.routes.firstWhereOrNull( (RouteBase e) => _debugIsDescendantOrSame( ancestor: e, route: defaultLocationRoute)); assert( match != null, - 'The defaultLocation (${branch.defaultLocation}) of ' + 'The defaultLocation (${branch.initialLocation}) of ' 'StatefulShellBranch must match a descendant route of the ' 'branch'); } @@ -183,7 +183,7 @@ class RouteConfiguration { route != null ? _fullPathForRoute(route, '', routes) : null; assert( defaultLocation != null, - 'The default location of a StatefulShellBranch must be derivable from ' + 'The initial location of a StatefulShellBranch must be derivable from ' 'GoRoute descendant'); return defaultLocation!; } diff --git a/packages/go_router/lib/src/misc/stateful_navigation_shell.dart b/packages/go_router/lib/src/misc/stateful_navigation_shell.dart index 8bea2cf504de..faef948b9e7b 100644 --- a/packages/go_router/lib/src/misc/stateful_navigation_shell.dart +++ b/packages/go_router/lib/src/misc/stateful_navigation_shell.dart @@ -106,7 +106,7 @@ class StatefulNavigationShellState extends State { } String _defaultBranchLocation(StatefulShellBranch branch) { - return branch.defaultLocation ?? + return branch.initialLocation ?? GoRouter.of(context) .routeConfiguration .findStatefulShellBranchDefaultLocation(branch); diff --git a/packages/go_router/lib/src/route.dart b/packages/go_router/lib/src/route.dart index 571ed0fa4e5f..48b25999f4e5 100644 --- a/packages/go_router/lib/src/route.dart +++ b/packages/go_router/lib/src/route.dart @@ -566,7 +566,7 @@ class ShellRoute extends ShellRouteBase { /// A StatefulShellRoute is created by specifying a List of [StatefulShellBranch] /// items, each representing a separate stateful branch in the route tree. /// StatefulShellBranch provides the root routes and the Navigator key ([GlobalKey]) -/// for the branch, as well as an optional default location. +/// for the branch, as well as an optional initial location. /// /// Like [ShellRoute], either a [builder] or a [pageBuilder] must be provided /// when creating a StatefulShellRoute. However, these builders differ in that @@ -708,15 +708,8 @@ class StatefulShellRoute extends ShellRouteBase { 'builder or pageBuilder must be provided'), assert(_debugUniqueNavigatorKeys(branches).length == branches.length, 'Navigator keys must be unique'), - super._(routes: _routes(branches)) { - for (int i = 0; i < routes.length; ++i) { - final RouteBase route = routes[i]; - if (route is GoRoute) { - assert(route.parentNavigatorKey == null || - route.parentNavigatorKey == branches[i].navigatorKey); - } - } - } + assert(_debugValidateParentNavigatorKeys(branches)), + super._(routes: _routes(branches)); /// The widget builder for a stateful shell route. /// @@ -797,6 +790,19 @@ class StatefulShellRoute extends ShellRouteBase { List branches) => Set>.from( branches.map((StatefulShellBranch e) => e.navigatorKey)); + + static bool _debugValidateParentNavigatorKeys( + List branches) { + for (final StatefulShellBranch branch in branches) { + for (final RouteBase route in branch.routes) { + if (route is GoRoute) { + assert(route.parentNavigatorKey == null || + route.parentNavigatorKey == branch.navigatorKey); + } + } + } + return true; + } } /// Builds the Widget managing a StatefulShellRoute. @@ -831,7 +837,7 @@ abstract class ShellNavigatorContainer extends StatelessWidget { /// /// The only required argument when creating a StatefulShellBranch is the /// sub-routes ([routes]), however sometimes it may be convenient to also -/// provide a [defaultLocation]. The value of this parameter is used when +/// 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 [StatefulShellBranchState]). A [navigatorKey] /// can be useful to provide in case it's necessary to access the [Navigator] @@ -842,14 +848,11 @@ class StatefulShellBranch { StatefulShellBranch({ required this.routes, GlobalKey? navigatorKey, - this.defaultLocation, - this.name, + this.initialLocation, this.restorationScopeId, this.observers, this.preload = false, - }) : navigatorKey = navigatorKey ?? - GlobalKey( - debugLabel: name != null ? 'Branch-$name' : null); + }) : navigatorKey = navigatorKey ?? GlobalKey(); /// The [GlobalKey] to be used by the [Navigator] built for this branch. /// @@ -862,23 +865,20 @@ class StatefulShellBranch { /// The list of child routes associated with this route branch. final List routes; - /// The default location for this route branch. + /// The initial location for this route branch. /// /// If none is specified, the location of the first descendant [GoRoute] will /// be used (i.e. first element in [routes], or a descendant). The default /// location is used when loading the branch for the first time (for instance /// when switching branch using the goBranch method in /// [StatefulShellBranchState]). - final String? defaultLocation; - - /// An optional name for this branch. - final String? name; + final String? initialLocation; /// Whether this route branch should be preloaded when the associated /// [StatefulShellRoute] is visited for the first time. /// /// If this is true, this branch will be preloaded by navigating to - /// the default location (see [defaultLocation]). The primary purpose of + /// the initial location (see [initialLocation]). 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 @@ -903,13 +903,12 @@ class StatefulShellBranch { return false; } return other.navigatorKey == navigatorKey && - other.defaultLocation == defaultLocation && - other.name == name && + other.initialLocation == initialLocation && other.preload == preload && other.restorationScopeId == restorationScopeId; } @override - int get hashCode => Object.hash( - navigatorKey, defaultLocation, name, preload, restorationScopeId); + int get hashCode => + Object.hash(navigatorKey, initialLocation, preload, restorationScopeId); } diff --git a/packages/go_router/lib/src/shell_state.dart b/packages/go_router/lib/src/shell_state.dart index bd415368a216..3fcb8cd44f8c 100644 --- a/packages/go_router/lib/src/shell_state.dart +++ b/packages/go_router/lib/src/shell_state.dart @@ -2,12 +2,10 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'package:collection/collection.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/widgets.dart'; import '../go_router.dart'; -import 'misc/errors.dart'; import 'misc/stateful_navigation_shell.dart'; /// The snapshot of the current state of a [StatefulShellRoute]. @@ -69,53 +67,19 @@ class StatefulShellRouteState { StatefulShellBranchState? branchState, bool navigateToDefaultLocation) _resetState; - StatefulShellBranchState _branchStateFor({ - GlobalKey? navigatorKey, - String? name, - int? index, - }) { - assert(navigatorKey != null || name != null || index != null); - assert([navigatorKey, name, index].whereNotNull().length == 1); - - final StatefulShellBranchState? state; - if (navigatorKey != null) { - state = branchStates.firstWhereOrNull((StatefulShellBranchState e) => - e.branch.navigatorKey == navigatorKey); - if (state == null) { - throw GoError('Unable to find branch with key $navigatorKey'); - } - } else if (name != null) { - state = branchStates.firstWhereOrNull( - (StatefulShellBranchState e) => e.branch.name == name); - if (state == null) { - throw GoError('Unable to find branch with name "$name"'); - } - } else { - state = branchStates[index!]; - } - return state; - } - /// Navigate to the current location of the shell navigator with the provided - /// Navigator key, name or index. + /// index. /// /// This method will switch the currently active [Navigator] for the /// [StatefulShellRoute] by replacing the current navigation stack with the - /// one of the route branch identified by the provided Navigator key, name or - /// index. If resetLocation is true, the branch will be reset to its default - /// location (see [StatefulShellBranch.defaultLocation]). + /// one of the route branch identified by the provided index. If resetLocation + /// is true, the branch will be reset to its initial location + /// (see [StatefulShellBranch.initialLocation]). void goBranch({ - GlobalKey? navigatorKey, - String? name, - int? index, + required int index, bool resetLocation = false, }) { - final StatefulShellBranchState state = _branchStateFor( - navigatorKey: navigatorKey, - name: name, - index: index, - ); - _switchActiveBranch(state, resetLocation); + _switchActiveBranch(branchStates[index], resetLocation); } /// Refreshes this StatefulShellRouteState by rebuilding the state for the @@ -128,28 +92,20 @@ class StatefulShellRouteState { /// the branches /// /// After the state has been reset, the current branch will navigated to its - /// default location, if [navigateToDefaultLocation] is true. + /// initial location, if [navigateToDefaultLocation] is true. void reset({bool navigateToDefaultLocation = true}) { _resetState(null, navigateToDefaultLocation); } - /// Resets the navigation state of the branch identified by the provided - /// Navigator key, name or index. + /// Resets the navigation state of the branch identified by the provided index. /// /// After the state has been reset, the branch will navigated to its - /// default location, if [navigateToDefaultLocation] is true. + /// initial location, if [navigateToDefaultLocation] is true. void resetBranch({ - GlobalKey? navigatorKey, - String? name, - int? index, + required int index, bool navigateToDefaultLocation = true, }) { - final StatefulShellBranchState state = _branchStateFor( - navigatorKey: navigatorKey, - name: name, - index: index, - ); - _resetState(state, navigateToDefaultLocation); + _resetState(branchStates[index], navigateToDefaultLocation); } @override diff --git a/packages/go_router/test/configuration_test.dart b/packages/go_router/test/configuration_test.dart index e7721982a3fa..11e9e492f0c1 100644 --- a/packages/go_router/test/configuration_test.dart +++ b/packages/go_router/test/configuration_test.dart @@ -215,7 +215,7 @@ void main() { routes: [ StatefulShellRoute(branches: [ StatefulShellBranch( - defaultLocation: '/x', + initialLocation: '/x', navigatorKey: sectionANavigatorKey, routes: [ GoRoute( @@ -261,7 +261,7 @@ void main() { routes: [ StatefulShellRoute(branches: [ StatefulShellBranch( - defaultLocation: '/b', + initialLocation: '/b', navigatorKey: sectionANavigatorKey, routes: [ GoRoute( @@ -271,7 +271,7 @@ void main() { ], ), StatefulShellBranch( - defaultLocation: '/b', + initialLocation: '/b', navigatorKey: sectionBNavigatorKey, routes: [ StatefulShellRoute(branches: [ @@ -322,7 +322,7 @@ void main() { ], ), StatefulShellBranch( - defaultLocation: '/b/detail', + initialLocation: '/b/detail', routes: [ GoRoute( path: '/b', @@ -336,7 +336,7 @@ void main() { ], ), StatefulShellBranch( - defaultLocation: '/c/detail', + initialLocation: '/c/detail', routes: [ StatefulShellRoute(branches: [ StatefulShellBranch( @@ -353,7 +353,7 @@ void main() { ], ), StatefulShellBranch( - defaultLocation: '/d/detail', + initialLocation: '/d/detail', routes: [ GoRoute( path: '/d', diff --git a/packages/go_router/test/go_router_test.dart b/packages/go_router/test/go_router_test.dart index 28d07d0aef34..96eba75c8859 100644 --- a/packages/go_router/test/go_router_test.dart +++ b/packages/go_router/test/go_router_test.dart @@ -14,7 +14,6 @@ import 'package:go_router/go_router.dart'; import 'package:go_router/src/delegate.dart'; import 'package:go_router/src/match.dart'; import 'package:go_router/src/matching.dart'; -import 'package:go_router/src/misc/errors.dart'; import 'package:logging/logging.dart'; import 'test_helpers.dart'; @@ -2937,10 +2936,6 @@ void main() { (WidgetTester tester) async { final GlobalKey rootNavigatorKey = GlobalKey(); - final GlobalKey branchANavigatorKey = - GlobalKey(); - final GlobalKey branchCNavigatorKey = - GlobalKey(); final GlobalKey statefulWidgetKey = GlobalKey(); StatefulShellRouteState? routeState; @@ -2965,7 +2960,6 @@ void main() { ], ), StatefulShellBranch( - name: 'B', routes: [ GoRoute( path: '/b', @@ -2975,7 +2969,6 @@ void main() { ], ), StatefulShellBranch( - navigatorKey: branchCNavigatorKey, routes: [ GoRoute( path: '/c', @@ -3005,14 +2998,14 @@ void main() { expect(find.text('Screen C'), findsNothing); expect(find.text('Screen D'), findsNothing); - routeState!.goBranch(name: 'B'); + routeState!.goBranch(index: 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(navigatorKey: branchCNavigatorKey); + routeState!.goBranch(index: 2); await tester.pumpAndSettle(); expect(find.text('Screen A'), findsNothing); expect(find.text('Screen B'), findsNothing); @@ -3026,21 +3019,6 @@ void main() { expect(find.text('Screen C'), findsNothing); expect(find.text('Screen D'), findsOneWidget); - expect(() { - // Verify that navigation without specifying name, key or index fails - routeState!.goBranch(); - }, throwsA(isAssertionError)); - - expect(() { - // Verify that navigation to unknown name fails - routeState!.goBranch(name: 'C'); - }, throwsA(isA())); - - expect(() { - // Verify that navigation to unknown name fails - routeState!.goBranch(navigatorKey: branchANavigatorKey); - }, throwsA(isA())); - expect(() { // Verify that navigation to unknown index fails routeState!.goBranch(index: 4); From 9f54b4ea21d48220eb4dcd449eb99bd658ad00b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20Lo=CC=88fstrand?= Date: Fri, 17 Feb 2023 16:41:28 +0100 Subject: [PATCH 082/112] Minor renaming. Removed commented out / obsolete test code. --- packages/go_router/lib/src/configuration.dart | 16 +- packages/go_router/test/builder_test.dart | 144 ------------------ .../go_router/test/configuration_test.dart | 8 +- 3 files changed, 12 insertions(+), 156 deletions(-) diff --git a/packages/go_router/lib/src/configuration.dart b/packages/go_router/lib/src/configuration.dart index 240c531298da..f5fd37cad07a 100644 --- a/packages/go_router/lib/src/configuration.dart +++ b/packages/go_router/lib/src/configuration.dart @@ -124,7 +124,7 @@ class RouteConfiguration { return true; } - // Check to see that the configured defaultLocation of StatefulShellBranches + // Check to see that the configured initialLocation of StatefulShellBranches // points to a descendant route of the route branch. bool _debugCheckStatefulShellBranchDefaultLocations( List routes, RouteMatcher matcher) { @@ -137,14 +137,14 @@ class RouteConfiguration { // throw assertion error if not found. findStatefulShellBranchDefaultLocation(branch); } else { - final RouteBase defaultLocationRoute = + final RouteBase initialLocationRoute = matcher.findMatch(branch.initialLocation!).last.route; final RouteBase? match = branch.routes.firstWhereOrNull( (RouteBase e) => _debugIsDescendantOrSame( - ancestor: e, route: defaultLocationRoute)); + ancestor: e, route: initialLocationRoute)); assert( match != null, - 'The defaultLocation (${branch.initialLocation}) of ' + 'The initialLocation (${branch.initialLocation}) of ' 'StatefulShellBranch must match a descendant route of the ' 'branch'); } @@ -155,7 +155,7 @@ class RouteConfiguration { } on MatcherError catch (e) { assert( false, - 'defaultLocation (${e.location}) of StatefulShellBranch must ' + 'initialLocation (${e.location}) of StatefulShellBranch must ' 'be a valid location'); } return true; @@ -179,13 +179,13 @@ class RouteConfiguration { /// find the first GoRoute, from which a full path will be derived. String findStatefulShellBranchDefaultLocation(StatefulShellBranch branch) { final GoRoute? route = _findFirstGoRoute(branch.routes); - final String? defaultLocation = + final String? initialLocation = route != null ? _fullPathForRoute(route, '', routes) : null; assert( - defaultLocation != null, + initialLocation != null, 'The initial location of a StatefulShellBranch must be derivable from ' 'GoRoute descendant'); - return defaultLocation!; + return initialLocation!; } static String? _fullPathForRoute( diff --git a/packages/go_router/test/builder_test.dart b/packages/go_router/test/builder_test.dart index c82831e9cfe5..97ff840f57eb 100644 --- a/packages/go_router/test/builder_test.dart +++ b/packages/go_router/test/builder_test.dart @@ -196,150 +196,6 @@ void main() { expect(find.byKey(key), findsOneWidget); }); - // testWidgets( - // 'throws when a branch of a StatefulShellRoute has an incorrect ' - // 'defaultLocation', (WidgetTester tester) async { - // final RouteConfiguration config = RouteConfiguration( - // routes: [ - // StatefulShellRoute( - // builder: (_, __, Widget child) { - // return _HomeScreen(child: child); - // }, - // branches: [ - // StatefulShellBranch(defaultLocation: '/x', routes: [ - // GoRoute( - // path: '/a', - // builder: (_, __) => _DetailsScreen(), - // ), - // ], - // ), - // StatefulShellBranch(defaultLocation: '/b', routes: [ - // GoRoute( - // path: '/b', - // builder: (_, __) => _DetailsScreen(), - // ), - // ], - // ), - // ]), - // ], - // redirectLimit: 10, - // topRedirect: (_, __) => null, - // navigatorKey: GlobalKey(), - // ); - // - // final RouteMatchList matches = RouteMatchList( - // [ - // _createRouteMatch(config.routes.first, '/b'), - // _createRouteMatch(config.routes.first.routes.first, '/b'), - // ], - // Uri.parse('/b'), - // const {}); - // - // await tester.pumpWidget( - // _BuilderTestWidget( - // routeConfiguration: config, - // matches: matches, - // ), - // ); - // - // expect(tester.takeException(), isAssertionError); - // }); - - // testWidgets( - // 'throws when a branch of a StatefulShellRoute has duplicate ' - // 'defaultLocation', (WidgetTester tester) async { - // final RouteConfiguration config = RouteConfiguration( - // routes: [ - // StatefulShellRoute( - // routes: [ - // GoRoute( - // path: '/a', - // builder: (_, __) => _DetailsScreen(), - // ), - // GoRoute( - // path: '/b', - // builder: (_, __) => _DetailsScreen(), - // ), - // ], - // builder: (_, __, Widget child) { - // return _HomeScreen(child: child); - // }, - // branches: [ - // StatefulShellBranch(rootLocation: '/a'), - // StatefulShellBranch(rootLocations: const ['/a', '/b']), - // ]), - // ], - // redirectLimit: 10, - // topRedirect: (_, __) => null, - // navigatorKey: GlobalKey(), - // ); - // - // final RouteMatchList matches = RouteMatchList( - // [ - // _createRouteMatch(config.routes.first, '/b'), - // _createRouteMatch(config.routes.first.routes.first, '/b'), - // ], - // Uri.parse('/b'), - // const {}); - // - // await tester.pumpWidget( - // _BuilderTestWidget( - // routeConfiguration: config, - // matches: matches, - // ), - // ); - // - // expect(tester.takeException(), isAssertionError); - // }); - - // testWidgets('throws when StatefulShellRoute has duplicate navigator keys', - // (WidgetTester tester) async { - // final GlobalKey keyA = - // GlobalKey(debugLabel: 'A'); - // final RouteConfiguration config = RouteConfiguration( - // routes: [ - // StatefulShellRoute( - // routes: [ - // GoRoute( - // path: '/a', - // builder: (_, __) => _DetailsScreen(), - // ), - // GoRoute( - // path: '/b', - // builder: (_, __) => _DetailsScreen(), - // ), - // ], - // builder: (_, __, Widget child) { - // return _HomeScreen(child: child); - // }, - // branches: [ - // StatefulShellBranch(rootLocation: '/a', navigatorKey: keyA), - // StatefulShellBranch(rootLocation: '/b', navigatorKey: keyA), - // ]), - // ], - // redirectLimit: 10, - // topRedirect: (_, __) => null, - // navigatorKey: GlobalKey(), - // ); - // - // final RouteMatchList matches = RouteMatchList( - // [ - // _createRouteMatch(config.routes.first, '/b'), - // _createRouteMatch(config.routes.first.routes.first, '/b'), - // ], - // Uri.parse('/b'), - // const {}); - // - // await tester.pumpWidget( - // _BuilderTestWidget( - // routeConfiguration: config, - // matches: matches, - // ), - // ); - // - // expect(tester.takeException(), isAssertionError); - // }); - testWidgets('Uses the correct navigatorKey', (WidgetTester tester) async { final GlobalKey rootNavigatorKey = GlobalKey(); diff --git a/packages/go_router/test/configuration_test.dart b/packages/go_router/test/configuration_test.dart index 11e9e492f0c1..5683aa12762e 100644 --- a/packages/go_router/test/configuration_test.dart +++ b/packages/go_router/test/configuration_test.dart @@ -201,7 +201,7 @@ void main() { test( 'throws when a branch of a StatefulShellRoute has an incorrect ' - 'defaultLocation', () { + 'initialLocation', () { final GlobalKey root = GlobalKey(debugLabel: 'root'); final GlobalKey sectionANavigatorKey = @@ -246,7 +246,7 @@ void main() { }); test( - 'throws when a branch of a StatefulShellRoute has a defaultLocation ' + 'throws when a branch of a StatefulShellRoute has a initialLocation ' 'that is not a descendant of the same branch', () { final GlobalKey root = GlobalKey(debugLabel: 'root'); @@ -300,7 +300,7 @@ void main() { test( 'does not throw when a branch of a StatefulShellRoute has correctly ' - 'configured defaultLocations', () { + 'configured initialLocations', () { final GlobalKey root = GlobalKey(debugLabel: 'root'); @@ -395,7 +395,7 @@ void main() { }); test( - 'derives the correct defaultLocation for a StatefulShellBranch', + 'derives the correct initialLocation for a StatefulShellBranch', () { final StatefulShellBranch branchA; final StatefulShellBranch branchY; From 1226f44da25ce2b271e3a1a64d5e66ebdd6baf3d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20Lo=CC=88fstrand?= Date: Sun, 19 Feb 2023 17:06:25 +0100 Subject: [PATCH 083/112] Updated equals method in GoRouterState to properly compare Map fields. --- packages/go_router/lib/src/state.dart | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/go_router/lib/src/state.dart b/packages/go_router/lib/src/state.dart index 1234e0d7d17a..aca0b8d4c6c2 100644 --- a/packages/go_router/lib/src/state.dart +++ b/packages/go_router/lib/src/state.dart @@ -2,6 +2,7 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'package:flutter/foundation.dart'; import 'package:flutter/widgets.dart'; import '../go_router.dart'; @@ -150,9 +151,9 @@ class GoRouterState { other.name == name && other.path == path && other.fullpath == fullpath && - other.params == params && - other.queryParams == queryParams && - other.queryParametersAll == queryParametersAll && + mapEquals(other.params, params) && + mapEquals(other.queryParams, queryParams) && + mapEquals(other.queryParametersAll, queryParametersAll) && other.extra == extra && other.error == error && other.pageKey == pageKey && From 881be86d2daf1c46727896bd18c7d9113f30a153 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20Lo=CC=88fstrand?= Date: Sun, 19 Feb 2023 17:08:00 +0100 Subject: [PATCH 084/112] Updated handling of RouteMatch lookup for pages/routes (replaced Expando with new class PagePopContext). --- packages/go_router/lib/src/builder.dart | 113 ++++++++++++------ packages/go_router/lib/src/delegate.dart | 4 +- .../go_router/lib/src/navigator_builder.dart | 23 ++-- packages/go_router/test/builder_test.dart | 2 +- 4 files changed, 89 insertions(+), 53 deletions(-) diff --git a/packages/go_router/lib/src/builder.dart b/packages/go_router/lib/src/builder.dart index da7e5253e7af..da3ac2341125 100644 --- a/packages/go_router/lib/src/builder.dart +++ b/packages/go_router/lib/src/builder.dart @@ -18,6 +18,44 @@ import 'pages/material.dart'; import 'route_data.dart'; import 'typedefs.dart'; +/// On pop page callback that includes the associated [RouteMatch]. +/// +/// This is a specialized version of [Navigator.onPopPage], used when creating +/// Navigators in [RouteBuilder]. +typedef RouteBuilderPopPageCallback = bool Function( + Route route, dynamic result, RouteMatch? match); + +/// Context used to provide a route to page association when popping routes. +class PagePopContext { + /// [PagePopContext] constructor. + PagePopContext(this.routeBuilderOnPopPage); + + final Map, RouteMatch> _routeMatchLookUp = + , RouteMatch>{}; + + /// On pop page callback that includes the associated [RouteMatch]. + final RouteBuilderPopPageCallback routeBuilderOnPopPage; + + /// Looks for the [RouteMatch] for a given [Page]. + /// + /// The [Page] must have been previously built via the [RouteBuilder] that + /// created this [PagePopContext]; otherwise, this method returns null. + RouteMatch? getRouteMatchForPage(Page page) => + _routeMatchLookUp[page]; + + void _setRouteMatchForPage(Page page, RouteMatch match) => + _routeMatchLookUp[page] = match; + + /// Function used as [Navigator.onPopPage] callback when creating Navigators. + /// + /// This function forwards to [routeBuilderOnPopPage], including the + /// [RouteMatch] associated with the popped route. + bool onPopPage(Route route, dynamic result) { + final Page page = route.settings as Page; + return routeBuilderOnPopPage(route, result, _routeMatchLookUp[page]); + } +} + /// Builds the top-level Navigator for GoRouter. class RouteBuilder { /// [RouteBuilder] constructor. @@ -52,20 +90,6 @@ class RouteBuilder { final GoRouterStateRegistry _registry = GoRouterStateRegistry(); - final Expando _routeMatchLookUp = Expando( - 'Page to RouteMatch', - ); - - /// Looks for the [RouteMatch] for a given [Page]. - /// - /// The [Page] must have been previously built via this [RouteBuilder]; - /// otherwise, this method returns null. - RouteMatch? getRouteMatchForPage(Page page) => - _routeMatchLookUp[page]; - - void _setRouteMatchForPage(Page page, RouteMatch match) => - _routeMatchLookUp[page] = match; - /// Caches a HeroController for the nested Navigator, which solves cases where the /// Hero Widget animation stops working when navigating. final Map, HeroController> _goHeroCache = @@ -75,7 +99,7 @@ class RouteBuilder { Widget build( BuildContext context, RouteMatchList matchList, - PopPageCallback onPopPage, + RouteBuilderPopPageCallback onPopPage, bool routerNeglect, ) { if (matchList.isEmpty) { @@ -112,16 +136,17 @@ class RouteBuilder { Widget tryBuild( BuildContext context, RouteMatchList matchList, - PopPageCallback onPopPage, + RouteBuilderPopPageCallback onPopPage, bool routerNeglect, GlobalKey navigatorKey, Map, GoRouterState> registry, ) { + final PagePopContext pagePopContext = PagePopContext(onPopPage); return builderWithNav( context, RouteNavigatorBuilder.buildNavigator( - onPopPage, - buildPages(context, matchList, 0, onPopPage, routerNeglect, + pagePopContext.onPopPage, + buildPages(context, matchList, 0, pagePopContext, routerNeglect, navigatorKey, registry), navigatorKey, observers: observers, @@ -136,7 +161,7 @@ class RouteBuilder { BuildContext context, RouteMatchList matchList, int startIndex, - PopPageCallback onPopPage, + PagePopContext pagePopContext, bool routerNeglect, GlobalKey navigatorKey, Map, GoRouterState> registry) { @@ -144,11 +169,11 @@ class RouteBuilder { , List>>{}; try { _buildRecursive(context, matchList.unmodifiableMatchList(), startIndex, - onPopPage, routerNeglect, keyToPage, navigatorKey, registry); + pagePopContext, routerNeglect, keyToPage, navigatorKey, registry); // Every Page should have a corresponding RouteMatch. - assert(keyToPage.values.flattened - .every((Page page) => getRouteMatchForPage(page) != null)); + assert(keyToPage.values.flattened.every((Page page) => + pagePopContext.getRouteMatchForPage(page) != null)); return keyToPage[navigatorKey]!; } on _RouteBuilderError catch (e) { return >[ @@ -167,7 +192,7 @@ class RouteBuilder { BuildContext context, RouteMatchList matchList, int startIndex, - PopPageCallback onPopPage, + RouteBuilderPopPageCallback onPopPage, bool routerNeglect, GlobalKey navigatorKey, {List? observers, @@ -177,10 +202,11 @@ class RouteBuilder { , GoRouterState>{}; final HeroController heroController = _goHeroCache.putIfAbsent( navigatorKey, () => _getHeroController(context)); + final PagePopContext pagePopContext = PagePopContext(onPopPage); final Widget result = RouteNavigatorBuilder.buildNavigator( - onPopPage, - buildPages(context, matchList, startIndex, onPopPage, routerNeglect, - navigatorKey, newRegistry), + pagePopContext.onPopPage, + buildPages(context, matchList, startIndex, pagePopContext, + routerNeglect, navigatorKey, newRegistry), navigatorKey, observers: observers, restorationScopeId: restorationScopeId, @@ -198,7 +224,7 @@ class RouteBuilder { BuildContext context, UnmodifiableRouteMatchList matchList, int startIndex, - PopPageCallback onPopPage, + PagePopContext pagePopContext, bool routerNeglect, Map, List>> keyToPages, GlobalKey navigatorKey, @@ -217,7 +243,8 @@ class RouteBuilder { final RouteBase route = match.route; final GoRouterState state = buildState(matchList, match); if (route is GoRoute) { - final Page page = _buildPageForRoute(context, state, match); + final Page page = + _buildPageForRoute(context, state, match, pagePopContext); registry[page] = state; // If this GoRoute is for a different Navigator, add it to the // list of out of scope pages @@ -226,7 +253,7 @@ class RouteBuilder { keyToPages.putIfAbsent(goRouteNavKey, () => >[]).add(page); - _buildRecursive(context, matchList, startIndex + 1, onPopPage, + _buildRecursive(context, matchList, startIndex + 1, pagePopContext, routerNeglect, keyToPages, navigatorKey, registry); } else if (route is ShellRouteBase) { assert(startIndex + 1 < matchList.matches.length, @@ -253,16 +280,26 @@ class RouteBuilder { keyToPages.putIfAbsent(shellNavigatorKey, () => >[]); // Build the remaining pages - _buildRecursive(context, matchList, startIndex + 1, onPopPage, + _buildRecursive(context, matchList, startIndex + 1, pagePopContext, routerNeglect, keyToPages, shellNavigatorKey, registry); final HeroController heroController = _goHeroCache.putIfAbsent( shellNavigatorKey, () => _getHeroController(context)); + // Build the Navigator builder for this shell route + final RouteNavigatorBuilder navigatorBuilder = RouteNavigatorBuilder( + this, + state, + route, + heroController, + shellNavigatorKey, + keyToPages[shellNavigatorKey]!, + pagePopContext); + // Build the Page for this route - final Page page = _buildPageForRoute(context, state, match, - child: RouteNavigatorBuilder(this, state, route, heroController, - shellNavigatorKey, keyToPages[shellNavigatorKey]!, onPopPage)); + final Page page = _buildPageForRoute( + context, state, match, pagePopContext, + child: navigatorBuilder); registry[page] = state; // Place the ShellRoute's Page onto the list for the parent navigator. keyToPages @@ -304,8 +341,8 @@ class RouteBuilder { } /// Builds a [Page] for [StackedRoute] - Page _buildPageForRoute( - BuildContext context, GoRouterState state, RouteMatch match, + Page _buildPageForRoute(BuildContext context, GoRouterState state, + RouteMatch match, PagePopContext pagePopContext, {Object? child}) { final RouteBase route = match.route; Page? page; @@ -329,7 +366,7 @@ class RouteBuilder { page ??= buildPage(context, state, Builder(builder: (BuildContext context) { return _callRouteBuilder(context, state, match, child: child); })); - _setRouteMatchForPage(page, match); + pagePopContext._setRouteMatchForPage(page, match); // Return the result of the route's builder() or pageBuilder() return page; @@ -442,10 +479,10 @@ class RouteBuilder { BuildContext context, _RouteBuilderError e, RouteMatchList matchList, - PopPageCallback onPopPage, + RouteBuilderPopPageCallback onPopPage, GlobalKey navigatorKey) { return RouteNavigatorBuilder.buildNavigator( - onPopPage, + (Route route, dynamic result) => onPopPage(route, result, null), >[ _buildErrorPage(context, e, matchList), ], diff --git a/packages/go_router/lib/src/delegate.dart b/packages/go_router/lib/src/delegate.dart index d84d1ff5e3a2..e89dac719efc 100644 --- a/packages/go_router/lib/src/delegate.dart +++ b/packages/go_router/lib/src/delegate.dart @@ -128,12 +128,10 @@ class GoRouterDelegate extends RouterDelegate ); } - bool _onPopPage(Route route, Object? result) { + bool _onPopPage(Route route, Object? result, RouteMatch? match) { if (!route.didPop(result)) { return false; } - final Page page = route.settings as Page; - final RouteMatch? match = builder.getRouteMatchForPage(page); if (match == null) { return true; } diff --git a/packages/go_router/lib/src/navigator_builder.dart b/packages/go_router/lib/src/navigator_builder.dart index e9010129f22b..43a866a3aa14 100644 --- a/packages/go_router/lib/src/navigator_builder.dart +++ b/packages/go_router/lib/src/navigator_builder.dart @@ -11,13 +11,14 @@ import 'parser.dart'; class RouteNavigatorBuilder extends ShellNavigatorBuilder { /// Constructs a NavigatorBuilder. RouteNavigatorBuilder( - this.routeBuilder, - this.state, - this.currentRoute, - this.heroController, - this.navigatorKeyForCurrentRoute, - this.pages, - this.onPopPage); + this.routeBuilder, + this.state, + this.currentRoute, + this.heroController, + this.navigatorKeyForCurrentRoute, + this.pages, + this.pagePopContext, + ); /// The route builder. final RouteBuilder routeBuilder; @@ -37,8 +38,8 @@ class RouteNavigatorBuilder extends ShellNavigatorBuilder { /// The pages for the current route. final List> pages; - /// The callback for popping a page. - final PopPageCallback onPopPage; + /// The page pop context. + final PagePopContext pagePopContext; /// Builds a navigator. static Widget buildNavigator( @@ -73,7 +74,7 @@ class RouteNavigatorBuilder extends ShellNavigatorBuilder { GlobalKey? navigatorKey, }) { return buildNavigator( - onPopPage, + pagePopContext.onPopPage, pages, navigatorKey ?? navigatorKeyForCurrentRoute, observers: observers, @@ -110,7 +111,7 @@ class RouteNavigatorBuilder extends ShellNavigatorBuilder { context, matchList, parentShellRouteIndex + 1, - onPopPage, + pagePopContext.routeBuilderOnPopPage, true, navigatorKey, restorationScopeId: restorationScopeId, diff --git a/packages/go_router/test/builder_test.dart b/packages/go_router/test/builder_test.dart index 97ff840f57eb..25eb319de7a5 100644 --- a/packages/go_router/test/builder_test.dart +++ b/packages/go_router/test/builder_test.dart @@ -542,7 +542,7 @@ class _BuilderTestWidget extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp( - home: builder.tryBuild(context, matches, (_, __) => false, false, + home: builder.tryBuild(context, matches, (_, __, ___) => false, false, routeConfiguration.navigatorKey, , GoRouterState>{}), // builder: (context, child) => , ); From fd5412f9bad9d511778b06e66a754454a409feea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20L=C3=B6fstrand?= Date: Thu, 23 Feb 2023 09:53:59 +0100 Subject: [PATCH 085/112] Add type annotation to listEquals Co-authored-by: chunhtai <47866232+chunhtai@users.noreply.github.com> --- packages/go_router/lib/src/matching.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/go_router/lib/src/matching.dart b/packages/go_router/lib/src/matching.dart index 147334e9736d..59674176c478 100644 --- a/packages/go_router/lib/src/matching.dart +++ b/packages/go_router/lib/src/matching.dart @@ -215,7 +215,7 @@ class UnmodifiableRouteMatchList implements RouteMatchList { if (other is! UnmodifiableRouteMatchList) { return false; } - return listEquals(other._matches, _matches) && + return listEquals(other._matches, _matches) && other._uri == _uri && mapEquals(other.pathParameters, pathParameters); } From a5234a6c670b18908e816e6ca04fada31b65ba49 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20Lo=CC=88fstrand?= Date: Sun, 26 Feb 2023 23:29:22 +0100 Subject: [PATCH 086/112] Moved RouteNavigatorBuilder into builder.dart and made private. Made PagePopContext private. --- packages/go_router/lib/src/builder.dart | 242 ++++++++++++++---- .../go_router/lib/src/navigator_builder.dart | 124 --------- 2 files changed, 191 insertions(+), 175 deletions(-) delete mode 100644 packages/go_router/lib/src/navigator_builder.dart diff --git a/packages/go_router/lib/src/builder.dart b/packages/go_router/lib/src/builder.dart index da3ac2341125..88782c204457 100644 --- a/packages/go_router/lib/src/builder.dart +++ b/packages/go_router/lib/src/builder.dart @@ -12,9 +12,9 @@ import 'logging.dart'; import 'match.dart'; import 'matching.dart'; import 'misc/error_screen.dart'; -import 'navigator_builder.dart'; import 'pages/cupertino.dart'; import 'pages/material.dart'; +import 'parser.dart'; import 'route_data.dart'; import 'typedefs.dart'; @@ -25,37 +25,6 @@ import 'typedefs.dart'; typedef RouteBuilderPopPageCallback = bool Function( Route route, dynamic result, RouteMatch? match); -/// Context used to provide a route to page association when popping routes. -class PagePopContext { - /// [PagePopContext] constructor. - PagePopContext(this.routeBuilderOnPopPage); - - final Map, RouteMatch> _routeMatchLookUp = - , RouteMatch>{}; - - /// On pop page callback that includes the associated [RouteMatch]. - final RouteBuilderPopPageCallback routeBuilderOnPopPage; - - /// Looks for the [RouteMatch] for a given [Page]. - /// - /// The [Page] must have been previously built via the [RouteBuilder] that - /// created this [PagePopContext]; otherwise, this method returns null. - RouteMatch? getRouteMatchForPage(Page page) => - _routeMatchLookUp[page]; - - void _setRouteMatchForPage(Page page, RouteMatch match) => - _routeMatchLookUp[page] = match; - - /// Function used as [Navigator.onPopPage] callback when creating Navigators. - /// - /// This function forwards to [routeBuilderOnPopPage], including the - /// [RouteMatch] associated with the popped route. - bool onPopPage(Route route, dynamic result) { - final Page page = route.settings as Page; - return routeBuilderOnPopPage(route, result, _routeMatchLookUp[page]); - } -} - /// Builds the top-level Navigator for GoRouter. class RouteBuilder { /// [RouteBuilder] constructor. @@ -141,12 +110,12 @@ class RouteBuilder { GlobalKey navigatorKey, Map, GoRouterState> registry, ) { - final PagePopContext pagePopContext = PagePopContext(onPopPage); + final _PagePopContext pagePopContext = _PagePopContext._(onPopPage); return builderWithNav( context, - RouteNavigatorBuilder.buildNavigator( + _RouteNavigatorBuilder.buildNavigator( pagePopContext.onPopPage, - buildPages(context, matchList, 0, pagePopContext, routerNeglect, + _buildPages(context, matchList, 0, pagePopContext, routerNeglect, navigatorKey, registry), navigatorKey, observers: observers, @@ -158,10 +127,21 @@ class RouteBuilder { /// testing. @visibleForTesting List> buildPages( + BuildContext context, + RouteMatchList matchList, + RouteBuilderPopPageCallback onPopPage, + int startIndex, + bool routerNeglect, + GlobalKey navigatorKey, + Map, GoRouterState> registry) => + _buildPages(context, matchList, startIndex, _PagePopContext._(onPopPage), + routerNeglect, navigatorKey, registry); + + List> _buildPages( BuildContext context, RouteMatchList matchList, int startIndex, - PagePopContext pagePopContext, + _PagePopContext pagePopContext, bool routerNeglect, GlobalKey navigatorKey, Map, GoRouterState> registry) { @@ -197,23 +177,27 @@ class RouteBuilder { GlobalKey navigatorKey, {List? observers, String? restorationScopeId}) { + final Map, List>> keyToPage = + , List>>{}; try { - final Map, GoRouterState> newRegistry = - , GoRouterState>{}; - final HeroController heroController = _goHeroCache.putIfAbsent( - navigatorKey, () => _getHeroController(context)); - final PagePopContext pagePopContext = PagePopContext(onPopPage); - final Widget result = RouteNavigatorBuilder.buildNavigator( + final _PagePopContext pagePopContext = _PagePopContext._(onPopPage); + _buildRecursive( + context, + matchList.unmodifiableMatchList(), + startIndex, + pagePopContext, + routerNeglect, + keyToPage, + navigatorKey, , GoRouterState>{}); + + return _RouteNavigatorBuilder.buildNavigator( pagePopContext.onPopPage, - buildPages(context, matchList, startIndex, pagePopContext, - routerNeglect, navigatorKey, newRegistry), + keyToPage[navigatorKey]!, navigatorKey, observers: observers, restorationScopeId: restorationScopeId, - heroController: heroController, + heroController: _getHeroController(context), ); - _registry.updateRegistry(newRegistry, replace: false); - return result; } on _RouteBuilderError catch (e) { return _buildErrorNavigator( context, e, matchList, onPopPage, configuration.navigatorKey); @@ -224,7 +208,7 @@ class RouteBuilder { BuildContext context, UnmodifiableRouteMatchList matchList, int startIndex, - PagePopContext pagePopContext, + _PagePopContext pagePopContext, bool routerNeglect, Map, List>> keyToPages, GlobalKey navigatorKey, @@ -287,7 +271,7 @@ class RouteBuilder { shellNavigatorKey, () => _getHeroController(context)); // Build the Navigator builder for this shell route - final RouteNavigatorBuilder navigatorBuilder = RouteNavigatorBuilder( + final _RouteNavigatorBuilder navigatorBuilder = _RouteNavigatorBuilder._( this, state, route, @@ -342,7 +326,7 @@ class RouteBuilder { /// Builds a [Page] for [StackedRoute] Page _buildPageForRoute(BuildContext context, GoRouterState state, - RouteMatch match, PagePopContext pagePopContext, + RouteMatch match, _PagePopContext pagePopContext, {Object? child}) { final RouteBase route = match.route; Page? page; @@ -481,7 +465,7 @@ class RouteBuilder { RouteMatchList matchList, RouteBuilderPopPageCallback onPopPage, GlobalKey navigatorKey) { - return RouteNavigatorBuilder.buildNavigator( + return _RouteNavigatorBuilder.buildNavigator( (Route route, dynamic result) => onPopPage(route, result, null), >[ _buildErrorPage(context, e, matchList), @@ -583,3 +567,159 @@ class _RouteBuilderException implements Exception { return '$message ${exception ?? ""}'; } } + +/// Context used to provide a route to page association when popping routes. +class _PagePopContext { + _PagePopContext._(this.routeBuilderOnPopPage); + + final Map, RouteMatch> _routeMatchLookUp = + , RouteMatch>{}; + + /// On pop page callback that includes the associated [RouteMatch]. + final RouteBuilderPopPageCallback routeBuilderOnPopPage; + + /// Looks for the [RouteMatch] for a given [Page]. + /// + /// The [Page] must have been previously built via the [RouteBuilder] that + /// created this [PagePopContext]; otherwise, this method returns null. + RouteMatch? getRouteMatchForPage(Page page) => + _routeMatchLookUp[page]; + + void _setRouteMatchForPage(Page page, RouteMatch match) => + _routeMatchLookUp[page] = match; + + /// Function used as [Navigator.onPopPage] callback when creating Navigators. + /// + /// This function forwards to [routeBuilderOnPopPage], including the + /// [RouteMatch] associated with the popped route. + bool onPopPage(Route route, dynamic result) { + final Page page = route.settings as Page; + return routeBuilderOnPopPage(route, result, _routeMatchLookUp[page]); + } +} + +/// Provides support for building Navigators for routes. +class _RouteNavigatorBuilder extends ShellNavigatorBuilder { + /// Constructs a NavigatorBuilder. + _RouteNavigatorBuilder._( + this.routeBuilder, + this.state, + this.currentRoute, + this.heroController, + this.navigatorKeyForCurrentRoute, + this.pages, + this.pagePopContext, + ); + + /// The route builder. + final RouteBuilder routeBuilder; + + @override + final GoRouterState state; + + @override + final ShellRouteBase currentRoute; + + /// The hero controller. + final HeroController heroController; + + @override + final GlobalKey navigatorKeyForCurrentRoute; + + /// The pages for the current route. + final List> pages; + + /// The page pop context. + final _PagePopContext pagePopContext; + + /// Builds a navigator. + static Widget buildNavigator( + PopPageCallback onPopPage, + List> pages, + Key? navigatorKey, { + List? observers, + String? restorationScopeId, + HeroController? heroController, + }) { + final Widget navigator = Navigator( + key: navigatorKey, + restorationScopeId: restorationScopeId, + pages: pages, + observers: observers ?? const [], + onPopPage: onPopPage, + ); + if (heroController != null) { + return HeroControllerScope( + controller: heroController, + child: navigator, + ); + } else { + return navigator; + } + } + + @override + Widget buildNavigatorForCurrentRoute({ + List? observers, + String? restorationScopeId, + GlobalKey? navigatorKey, + }) { + return buildNavigator( + pagePopContext.onPopPage, + pages, + navigatorKey ?? navigatorKeyForCurrentRoute, + observers: observers, + restorationScopeId: restorationScopeId, + heroController: heroController, + ); + } + + @override + Future buildPreloadedShellNavigator({ + required BuildContext context, + required String location, + required GlobalKey navigatorKey, + required ShellRouteBase parentShellRoute, + List? observers, + String? restorationScopeId, + }) { + // Parse a RouteMatchList from location and handle any redirects + final GoRouteInformationParser parser = + GoRouter.of(context).routeInformationParser; + final Future routeMatchList = + parser.parseRouteInformationWithDependencies( + RouteInformation(location: location), + context, + ); + + Widget? buildNavigator(RouteMatchList matchList) { + // Find the index of fromRoute in the match list + final int parentShellRouteIndex = matchList.matches + .indexWhere((RouteMatch e) => e.route == parentShellRoute); + if (parentShellRouteIndex >= 0) { + final int startIndex = parentShellRouteIndex + 1; + final GlobalKey routeNavigatorKey = parentShellRoute + .navigatorKeyForSubRoute(matchList.matches[startIndex].route); + assert( + navigatorKey == routeNavigatorKey, + 'Incorrect shell navigator key ' + 'for preloaded match list for location "$location"'); + + return routeBuilder.buildPreloadedNestedNavigator( + context, + matchList, + parentShellRouteIndex + 1, + pagePopContext.routeBuilderOnPopPage, + true, + navigatorKey, + restorationScopeId: restorationScopeId, + observers: observers, + ); + } else { + return null; + } + } + + return routeMatchList.then(buildNavigator); + } +} diff --git a/packages/go_router/lib/src/navigator_builder.dart b/packages/go_router/lib/src/navigator_builder.dart deleted file mode 100644 index 43a866a3aa14..000000000000 --- a/packages/go_router/lib/src/navigator_builder.dart +++ /dev/null @@ -1,124 +0,0 @@ -import 'package:flutter/cupertino.dart'; - -import '../go_router.dart'; -import 'builder.dart'; -import 'configuration.dart'; -import 'match.dart'; -import 'matching.dart'; -import 'parser.dart'; - -/// Provides support for building Navigators for routes. -class RouteNavigatorBuilder extends ShellNavigatorBuilder { - /// Constructs a NavigatorBuilder. - RouteNavigatorBuilder( - this.routeBuilder, - this.state, - this.currentRoute, - this.heroController, - this.navigatorKeyForCurrentRoute, - this.pages, - this.pagePopContext, - ); - - /// The route builder. - final RouteBuilder routeBuilder; - - @override - final GoRouterState state; - - @override - final ShellRouteBase currentRoute; - - /// The hero controller. - final HeroController heroController; - - @override - final GlobalKey navigatorKeyForCurrentRoute; - - /// The pages for the current route. - final List> pages; - - /// The page pop context. - final PagePopContext pagePopContext; - - /// Builds a navigator. - static Widget buildNavigator( - PopPageCallback onPopPage, - List> pages, - Key? navigatorKey, { - List? observers, - String? restorationScopeId, - HeroController? heroController, - }) { - final Widget navigator = Navigator( - key: navigatorKey, - restorationScopeId: restorationScopeId, - pages: pages, - observers: observers ?? const [], - onPopPage: onPopPage, - ); - if (heroController != null) { - return HeroControllerScope( - controller: heroController, - child: navigator, - ); - } else { - return navigator; - } - } - - @override - Widget buildNavigatorForCurrentRoute({ - List? observers, - String? restorationScopeId, - GlobalKey? navigatorKey, - }) { - return buildNavigator( - pagePopContext.onPopPage, - pages, - navigatorKey ?? navigatorKeyForCurrentRoute, - observers: observers, - restorationScopeId: restorationScopeId, - heroController: heroController, - ); - } - - @override - Future buildPreloadedShellNavigator({ - required BuildContext context, - required String location, - Object? extra, - required GlobalKey navigatorKey, - required ShellRouteBase parentShellRoute, - List? observers, - String? restorationScopeId, - }) { - // Parse a RouteMatchList from location and handle any redirects - final GoRouteInformationParser parser = - GoRouter.of(context).routeInformationParser; - final Future routeMatchList = - parser.parseRouteInformationWithDependencies( - RouteInformation(location: location, state: extra), - context, - ); - - Widget buildNavigator(RouteMatchList matchList) { - // Find the index of fromRoute in the match list - final int parentShellRouteIndex = matchList.matches - .indexWhere((RouteMatch e) => e.route == parentShellRoute); - assert(parentShellRouteIndex >= 0); - return routeBuilder.buildPreloadedNestedNavigator( - context, - matchList, - parentShellRouteIndex + 1, - pagePopContext.routeBuilderOnPopPage, - true, - navigatorKey, - restorationScopeId: restorationScopeId, - observers: observers, - ); - } - - return routeMatchList.then(buildNavigator); - } -} From b9428cb7027bea7570deada0a4ceb40bc76d6fee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20Lo=CC=88fstrand?= Date: Sun, 26 Feb 2023 23:34:32 +0100 Subject: [PATCH 087/112] Moved StatefulNavigationShell and support classes into route.dart and made private. Moved implementation details out of StatefulShellRouteState and StatefulShellBranchState, and into private implementation classes in route.dart. Reverted changes to GoRouterStateRegistry (no longer needed). --- packages/go_router/lib/src/configuration.dart | 13 +- .../src/misc/stateful_navigation_shell.dart | 397 ------------- packages/go_router/lib/src/route.dart | 532 +++++++++++++++++- packages/go_router/lib/src/shell_state.dart | 146 +---- packages/go_router/lib/src/state.dart | 31 +- packages/go_router/test/go_router_test.dart | 140 +---- 6 files changed, 555 insertions(+), 704 deletions(-) delete mode 100644 packages/go_router/lib/src/misc/stateful_navigation_shell.dart diff --git a/packages/go_router/lib/src/configuration.dart b/packages/go_router/lib/src/configuration.dart index f5fd37cad07a..b16f35930ed7 100644 --- a/packages/go_router/lib/src/configuration.dart +++ b/packages/go_router/lib/src/configuration.dart @@ -161,13 +161,16 @@ class RouteConfiguration { return true; } - static Iterable _subRoutesRecursively(List routes) => - routes.expand( - (RouteBase e) => [e, ..._subRoutesRecursively(e.routes)]); + static Iterable _subRoutesRecursively( + List routes) sync* { + for (final RouteBase route in routes) { + yield route; + yield* _subRoutesRecursively(route.routes); + } + } static GoRoute? _findFirstGoRoute(List routes) => - _subRoutesRecursively(routes) - .firstWhereOrNull((RouteBase e) => e is GoRoute) as GoRoute?; + _subRoutesRecursively(routes).whereType().firstOrNull; /// Tests if a route is a descendant of, or same as, an ancestor route. bool _debugIsDescendantOrSame( diff --git a/packages/go_router/lib/src/misc/stateful_navigation_shell.dart b/packages/go_router/lib/src/misc/stateful_navigation_shell.dart deleted file mode 100644 index faef948b9e7b..000000000000 --- a/packages/go_router/lib/src/misc/stateful_navigation_shell.dart +++ /dev/null @@ -1,397 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'package:collection/collection.dart'; -import 'package:flutter/widgets.dart'; - -import '../../go_router.dart'; - -/// [InheritedWidget] for providing a reference to the closest -/// [StatefulNavigationShellState]. -class InheritedStatefulNavigationShell extends InheritedWidget { - /// Constructs an [InheritedStatefulNavigationShell]. - const InheritedStatefulNavigationShell({ - required super.child, - required this.routeState, - super.key, - }); - - /// The [StatefulShellRouteState] that is exposed by this InheritedWidget. - final StatefulShellRouteState routeState; - - @override - bool updateShouldNotify( - covariant InheritedStatefulNavigationShell oldWidget) { - return routeState != oldWidget.routeState; - } -} - -/// Widget that manages and maintains the state of a [StatefulShellRoute], -/// including the [Navigator]s of the configured route branches. -/// -/// This widget acts as a wrapper around the builder function specified for the -/// associated StatefulShellRoute, and exposes the state (represented by -/// [StatefulShellRouteState]) to its child widgets with the help of the -/// InheritedWidget [InheritedStatefulNavigationShell]. The state for each route -/// branch is represented by [StatefulShellBranchState] and can be accessed via the -/// StatefulShellRouteState. -/// -/// By default, this widget creates a container for the branch route Navigators, -/// provided as the child argument to the builder of the StatefulShellRoute. -/// However, implementors can choose to disregard this and use an alternate -/// container around the branch navigators -/// (see [StatefulShellRouteState.children]) instead. -class StatefulNavigationShell extends StatefulWidget { - /// Constructs an [StatefulNavigationShell]. - const StatefulNavigationShell({ - required this.shellRoute, - required this.navigatorBuilder, - required this.shellBodyWidgetBuilder, - super.key, - }); - - /// The associated [StatefulShellRoute] - final StatefulShellRoute shellRoute; - - /// The shell navigator builder. - final ShellNavigatorBuilder navigatorBuilder; - - /// The shell body widget builder. - final ShellBodyWidgetBuilder shellBodyWidgetBuilder; - - @override - State createState() => StatefulNavigationShellState(); -} - -/// State for StatefulNavigationShell. -class StatefulNavigationShellState extends State { - final Map _navigatorCache = {}; - - late StatefulShellRouteState _routeState; - - List get _branches => widget.shellRoute.branches; - - GoRouterState get _currentGoRouterState => widget.navigatorBuilder.state; - GlobalKey get _currentNavigatorKey => - widget.navigatorBuilder.navigatorKeyForCurrentRoute; - - Widget? _navigatorForBranch(StatefulShellBranch branch) { - return _navigatorCache[branch.navigatorKey]; - } - - void _setNavigatorForBranch(StatefulShellBranch branch, Widget? navigator) { - navigator != null - ? _navigatorCache[branch.navigatorKey] = navigator - : _navigatorCache.remove(branch.navigatorKey); - } - - int _findCurrentIndex() { - final int index = _branches.indexWhere( - (StatefulShellBranch e) => e.navigatorKey == _currentNavigatorKey); - assert(index >= 0); - return index; - } - - void _switchActiveBranch( - StatefulShellBranchState branchState, bool resetLocation) { - final GoRouter goRouter = GoRouter.of(context); - final GoRouterState? routeState = branchState.routeState; - if (routeState != null && !resetLocation) { - goRouter.goState(routeState, context).onError( - (_, __) => goRouter.go(_defaultBranchLocation(branchState.branch))); - } else { - goRouter.go(_defaultBranchLocation(branchState.branch)); - } - } - - String _defaultBranchLocation(StatefulShellBranch branch) { - return branch.initialLocation ?? - GoRouter.of(context) - .routeConfiguration - .findStatefulShellBranchDefaultLocation(branch); - } - - void _preloadBranches() { - final List states = _routeState.branchStates; - for (StatefulShellBranchState state in states) { - if (state.branch.preload && !state.isLoaded) { - state = _updateStatefulShellBranchState(state, loaded: true); - _preloadBranch(state).then((StatefulShellBranchState navigatorState) { - setState(() { - _updateRouteBranchState(navigatorState); - }); - }); - } - } - } - - Future _preloadBranch( - StatefulShellBranchState branchState) { - final Future navigatorBuilder = - widget.navigatorBuilder.buildPreloadedShellNavigator( - context: context, - location: _defaultBranchLocation(branchState.branch), - parentShellRoute: widget.shellRoute, - navigatorKey: branchState.navigatorKey, - observers: branchState.branch.observers, - restorationScopeId: branchState.branch.restorationScopeId, - ); - - return navigatorBuilder.then((Widget navigator) { - return _updateStatefulShellBranchState( - branchState, - navigator: navigator, - ); - }); - } - - void _updateRouteBranchState(StatefulShellBranchState branchState, - {int? currentIndex}) { - final List existingStates = - _routeState.branchStates; - final List newStates = - []; - - // Build a new list of the current StatefulShellBranchStates, with an - // updated state for the current branch etc. - for (final StatefulShellBranch branch in _branches) { - if (branch.navigatorKey == branchState.navigatorKey) { - newStates.add(branchState); - } else { - newStates.add(existingStates.firstWhereOrNull( - (StatefulShellBranchState e) => e.branch == branch) ?? - _createStatefulShellBranchState(branch)); - } - } - - // Remove any obsolete cached Navigators - final Set validKeys = - _branches.map((StatefulShellBranch e) => e.navigatorKey).toSet(); - _navigatorCache.removeWhere((Key key, _) => !validKeys.contains(key)); - - _routeState = _routeState.copy( - branchStates: newStates, - currentIndex: currentIndex, - ); - } - - void _updateRouteStateFromWidget() { - final int index = _findCurrentIndex(); - final StatefulShellBranch branch = _branches[index]; - - final Widget currentNavigator = - widget.navigatorBuilder.buildNavigatorForCurrentRoute( - observers: branch.observers, - restorationScopeId: branch.restorationScopeId, - ); - - // Update or create a new StatefulShellBranchState for the current branch - // (i.e. the arguments currently provided to the Widget). - StatefulShellBranchState? currentBranchState = _routeState.branchStates - .firstWhereOrNull((StatefulShellBranchState e) => e.branch == branch); - if (currentBranchState != null) { - currentBranchState = _updateStatefulShellBranchState( - currentBranchState, - navigator: currentNavigator, - routeState: _currentGoRouterState, - ); - } else { - currentBranchState = _createStatefulShellBranchState( - branch, - navigator: currentNavigator, - routeState: _currentGoRouterState, - ); - } - - _updateRouteBranchState( - currentBranchState, - currentIndex: index, - ); - - _preloadBranches(); - } - - void _resetState( - StatefulShellBranchState? branchState, bool navigateToDefaultLocation) { - final StatefulShellBranch branch; - if (branchState != null) { - branch = branchState.branch; - _setNavigatorForBranch(branch, null); - _updateRouteBranchState( - _createStatefulShellBranchState(branch), - ); - } else { - branch = _routeState.currentBranchState.branch; - // Reset the state for all branches (the whole stateful shell) - _navigatorCache.clear(); - _setupInitialStatefulShellRouteState(); - } - if (navigateToDefaultLocation) { - GoRouter.of(context).go(_defaultBranchLocation(branch)); - } - } - - StatefulShellBranchState _updateStatefulShellBranchState( - StatefulShellBranchState branchState, { - Widget? navigator, - GoRouterState? routeState, - bool? loaded, - }) { - bool dirty = false; - if (routeState != null) { - dirty = branchState.routeState != routeState; - } - - if (navigator != null) { - // Only update Navigator for branch if matchList is different (i.e. - // dirty == true) or if Navigator didn't already exist - final bool hasExistingNav = - _navigatorForBranch(branchState.branch) != null; - if (!hasExistingNav || dirty) { - dirty = true; - _setNavigatorForBranch(branchState.branch, navigator); - } - } - - final bool isLoaded = - loaded ?? _navigatorForBranch(branchState.branch) != null; - dirty = dirty || isLoaded != branchState.isLoaded; - - if (dirty) { - return branchState.copy( - isLoaded: isLoaded, - routeState: routeState, - ); - } else { - return branchState; - } - } - - StatefulShellBranchState _createStatefulShellBranchState( - StatefulShellBranch branch, { - Widget? navigator, - GoRouterState? routeState, - }) { - if (navigator != null) { - _setNavigatorForBranch(branch, navigator); - } - return StatefulShellBranchState( - branch: branch, - routeState: routeState, - ); - } - - void _setupInitialStatefulShellRouteState() { - final List states = _branches - .map((StatefulShellBranch e) => _createStatefulShellBranchState(e)) - .toList(); - - _routeState = StatefulShellRouteState( - route: widget.shellRoute, - branchStates: states, - currentIndex: 0, - switchActiveBranch: _switchActiveBranch, - resetState: _resetState, - ); - } - - @override - void initState() { - super.initState(); - _setupInitialStatefulShellRouteState(); - } - - @override - void didUpdateWidget(covariant StatefulNavigationShell oldWidget) { - super.didUpdateWidget(oldWidget); - _updateRouteStateFromWidget(); - } - - @override - void didChangeDependencies() { - super.didChangeDependencies(); - _updateRouteStateFromWidget(); - } - - @override - Widget build(BuildContext context) { - final List children = _branches - .map((StatefulShellBranch branch) => _BranchNavigatorProxy( - branch: branch, navigatorForBranch: _navigatorForBranch)) - .toList(); - - return InheritedStatefulNavigationShell( - routeState: _routeState, - child: Builder(builder: (BuildContext context) { - // This Builder Widget is mainly used to make it possible to access the - // StatefulShellRouteState via the BuildContext in the ShellRouteBuilder - final ShellBodyWidgetBuilder shellWidgetBuilder = - widget.shellBodyWidgetBuilder; - return shellWidgetBuilder( - context, - _currentGoRouterState, - _IndexedStackedRouteBranchContainer( - routeState: _routeState, children: children), - ); - }), - ); - } -} - -typedef _NavigatorForBranch = Widget? Function(StatefulShellBranch); - -/// Widget that serves as the proxy for a branch Navigator Widget, which -/// possibly hasn't been created yet. -class _BranchNavigatorProxy extends StatelessWidget { - const _BranchNavigatorProxy({ - required this.branch, - required this.navigatorForBranch, - }); - - final StatefulShellBranch branch; - final _NavigatorForBranch navigatorForBranch; - - @override - Widget build(BuildContext context) { - return navigatorForBranch(branch) ?? const SizedBox.shrink(); - } -} - -/// Default implementation of a container widget for the [Navigator]s of the -/// route branches. This implementation uses an [IndexedStack] as a container. -class _IndexedStackedRouteBranchContainer extends ShellNavigatorContainer { - const _IndexedStackedRouteBranchContainer( - {required this.routeState, required this.children}); - - final StatefulShellRouteState routeState; - - @override - final List children; - - @override - Widget build(BuildContext context) { - final int currentIndex = routeState.currentIndex; - final List stackItems = children - .mapIndexed((int index, Widget child) => - _buildRouteBranchContainer(context, currentIndex == index, child)) - .toList(); - - return IndexedStack(index: currentIndex, children: stackItems); - } - - Widget _buildRouteBranchContainer( - BuildContext context, bool isActive, Widget child) { - return Offstage( - offstage: !isActive, - child: TickerMode( - enabled: isActive, - child: child, - ), - ); - } -} - -extension _StatefulShellBranchStateHelper on StatefulShellBranchState { - GlobalKey get navigatorKey => branch.navigatorKey; -} diff --git a/packages/go_router/lib/src/route.dart b/packages/go_router/lib/src/route.dart index 48b25999f4e5..f54bca84b50d 100644 --- a/packages/go_router/lib/src/route.dart +++ b/packages/go_router/lib/src/route.dart @@ -3,14 +3,12 @@ // found in the LICENSE file. import 'package:collection/collection.dart'; +import 'package:flutter/foundation.dart'; import 'package:flutter/widgets.dart'; import 'package:meta/meta.dart'; -import 'configuration.dart'; -import 'misc/stateful_navigation_shell.dart'; -import 'pages/custom_transition_page.dart'; +import '../go_router.dart'; import 'path_utils.dart'; -import 'typedefs.dart'; /// The base class for [GoRoute] and [ShellRoute]. /// @@ -366,10 +364,9 @@ abstract class ShellNavigatorBuilder { }); /// Builds a preloaded [Navigator] for a specific location. - Future buildPreloadedShellNavigator({ + Future buildPreloadedShellNavigator({ required BuildContext context, required String location, - Object? extra, required GlobalKey navigatorKey, required ShellRouteBase parentShellRoute, List? observers, @@ -815,7 +812,7 @@ class StatefulShellBuilder { /// Builds the Widget managing a StatefulShellRoute. Widget buildShell(ShellBodyWidgetBuilder body) { - return StatefulNavigationShell( + return _StatefulNavigationShell._( shellRoute: _shellRoute, navigatorBuilder: _builder, shellBodyWidgetBuilder: body, @@ -893,22 +890,529 @@ class StatefulShellBranch { /// /// The observers parameter is used by the [Navigator] built for this branch. final List? observers; +} + +/// [StatefulShellRouteState] extension, providing support for fetching the state +/// associated with the nearest [StatefulShellRoute] in the Widget tree. +extension StatefulShellRouteStateContext on StatefulShellRouteState { + /// Gets the state for the nearest stateful shell route in the Widget tree. + static StatefulShellRouteState of(BuildContext context) { + final _InheritedStatefulNavigationShell? inherited = + context.dependOnInheritedWidgetOfExactType< + _InheritedStatefulNavigationShell>(); + assert(inherited != null, + 'No InheritedStatefulNavigationShell found in context'); + return inherited!.routeState; + } +} + +/// [InheritedWidget] for providing a reference to the closest +/// [_StatefulNavigationShellState]. +class _InheritedStatefulNavigationShell extends InheritedWidget { + /// Constructs an [_InheritedStatefulNavigationShell]. + const _InheritedStatefulNavigationShell({ + required super.child, + required this.routeState, + }); + + /// The [StatefulShellRouteState] that is exposed by this InheritedWidget. + final StatefulShellRouteState routeState; + + @override + bool updateShouldNotify( + covariant _InheritedStatefulNavigationShell oldWidget) { + return routeState != oldWidget.routeState; + } +} + +/// Widget that manages and maintains the state of a [StatefulShellRoute], +/// including the [Navigator]s of the configured route branches. +/// +/// This widget acts as a wrapper around the builder function specified for the +/// associated StatefulShellRoute, and exposes the state (represented by +/// [StatefulShellRouteState]) to its child widgets with the help of the +/// InheritedWidget [_InheritedStatefulNavigationShell]. The state for each route +/// branch is represented by [StatefulShellBranchState] and can be accessed via the +/// StatefulShellRouteState. +/// +/// By default, this widget creates a container for the branch route Navigators, +/// provided as the child argument to the builder of the StatefulShellRoute. +/// However, implementors can choose to disregard this and use an alternate +/// container around the branch navigators +/// (see [StatefulShellRouteState.children]) instead. +class _StatefulNavigationShell extends StatefulWidget { + /// Constructs an [_StatefulNavigationShell]. + const _StatefulNavigationShell._({ + required this.shellRoute, + required this.navigatorBuilder, + required this.shellBodyWidgetBuilder, + }); + + /// The associated [StatefulShellRoute] + final StatefulShellRoute shellRoute; + + /// The shell navigator builder. + final ShellNavigatorBuilder navigatorBuilder; + + /// The shell body widget builder. + final ShellBodyWidgetBuilder shellBodyWidgetBuilder; + + @override + State createState() => _StatefulNavigationShellState(); +} + +/// State for StatefulNavigationShell. +class _StatefulNavigationShellState extends State<_StatefulNavigationShell> { + final Map _navigatorCache = {}; + + late _StatefulShellRouteState _routeState; + + List get _branches => widget.shellRoute.branches; + + GoRouterState get _currentGoRouterState => widget.navigatorBuilder.state; + GlobalKey get _currentNavigatorKey => + widget.navigatorBuilder.navigatorKeyForCurrentRoute; + + Widget? _navigatorForBranch(StatefulShellBranch branch) { + return _navigatorCache[branch.navigatorKey]; + } + + void _setNavigatorForBranch(StatefulShellBranch branch, Widget? navigator) { + navigator != null + ? _navigatorCache[branch.navigatorKey] = navigator + : _navigatorCache.remove(branch.navigatorKey); + } + + int _findCurrentIndex() { + final int index = _branches.indexWhere( + (StatefulShellBranch e) => e.navigatorKey == _currentNavigatorKey); + assert(index >= 0); + return index; + } + + void _switchActiveBranch(StatefulShellBranchState branchState) { + final GoRouter goRouter = GoRouter.of(context); + final GoRouterState? routeState = branchState.routeState; + if (routeState != null) { + goRouter.goState(routeState, context).onError( + (_, __) => goRouter.go(_defaultBranchLocation(branchState.branch))); + } else { + goRouter.go(_defaultBranchLocation(branchState.branch)); + } + } + + String _defaultBranchLocation(StatefulShellBranch branch) { + return branch.initialLocation ?? + GoRouter.of(context) + .routeConfiguration + .findStatefulShellBranchDefaultLocation(branch); + } + + void _preloadBranches() { + final List<_StatefulShellBranchState> states = _routeState._branchStates; + for (_StatefulShellBranchState state in states) { + if (state.branch.preload && !state.isLoaded) { + state = _updateStatefulShellBranchState(state, loaded: true); + _preloadBranch(state).then((_StatefulShellBranchState branchState) { + setState(() { + _updateRouteBranchState(branchState); + }); + }); + } + } + } + + Future<_StatefulShellBranchState> _preloadBranch( + _StatefulShellBranchState branchState) { + final Future navigatorBuilder = + widget.navigatorBuilder.buildPreloadedShellNavigator( + context: context, + location: _defaultBranchLocation(branchState.branch), + parentShellRoute: widget.shellRoute, + navigatorKey: branchState.navigatorKey, + observers: branchState.branch.observers, + restorationScopeId: branchState.branch.restorationScopeId, + ); + + return navigatorBuilder.then((Widget? navigator) { + return _updateStatefulShellBranchState( + branchState, + navigator: navigator, + ); + }); + } + + void _updateRouteBranchState(_StatefulShellBranchState branchState, + {int? currentIndex}) { + final List<_StatefulShellBranchState> existingStates = + _routeState._branchStates; + final List<_StatefulShellBranchState> newStates = + <_StatefulShellBranchState>[]; + + // Build a new list of the current StatefulShellBranchStates, with an + // updated state for the current branch etc. + for (final StatefulShellBranch branch in _branches) { + if (branch.navigatorKey == branchState.navigatorKey) { + newStates.add(branchState); + } else { + newStates.add(existingStates.firstWhereOrNull( + (StatefulShellBranchState e) => e.branch == branch) ?? + _createStatefulShellBranchState(branch)); + } + } + + // Remove any obsolete cached Navigators + final Set validKeys = + _branches.map((StatefulShellBranch e) => e.navigatorKey).toSet(); + _navigatorCache.removeWhere((Key key, _) => !validKeys.contains(key)); + + _routeState = _routeState._copy( + branchStates: newStates, + currentIndex: currentIndex, + ); + } + + void _updateRouteStateFromWidget() { + final int index = _findCurrentIndex(); + final StatefulShellBranch branch = _branches[index]; + + final Widget currentNavigator = + widget.navigatorBuilder.buildNavigatorForCurrentRoute( + observers: branch.observers, + restorationScopeId: branch.restorationScopeId, + ); + + // Update or create a new StatefulShellBranchState for the current branch + // (i.e. the arguments currently provided to the Widget). + _StatefulShellBranchState? currentBranchState = _routeState._branchStates + .firstWhereOrNull((_StatefulShellBranchState e) => e.branch == branch); + if (currentBranchState != null) { + currentBranchState = _updateStatefulShellBranchState( + currentBranchState, + navigator: currentNavigator, + routeState: _currentGoRouterState, + ); + } else { + currentBranchState = _createStatefulShellBranchState( + branch, + navigator: currentNavigator, + routeState: _currentGoRouterState, + ); + } + + _updateRouteBranchState( + currentBranchState, + currentIndex: index, + ); + + _preloadBranches(); + } + + _StatefulShellBranchState _updateStatefulShellBranchState( + _StatefulShellBranchState branchState, { + Widget? navigator, + GoRouterState? routeState, + bool? loaded, + }) { + bool dirty = false; + if (routeState != null) { + dirty = branchState.routeState != routeState; + } + + if (navigator != null) { + // Only update Navigator for branch if matchList is different (i.e. + // dirty == true) or if Navigator didn't already exist + final bool hasExistingNav = + _navigatorForBranch(branchState.branch) != null; + if (!hasExistingNav || dirty) { + dirty = true; + _setNavigatorForBranch(branchState.branch, navigator); + } + } + + final bool isLoaded = + loaded ?? _navigatorForBranch(branchState.branch) != null; + dirty = dirty || isLoaded != branchState.isLoaded; + + if (dirty) { + return branchState._copy( + isLoaded: isLoaded, + routeState: routeState, + ); + } else { + return branchState; + } + } + + _StatefulShellBranchState _createStatefulShellBranchState( + StatefulShellBranch branch, { + Widget? navigator, + GoRouterState? routeState, + }) { + if (navigator != null) { + _setNavigatorForBranch(branch, navigator); + } + return _StatefulShellBranchState._( + branch: branch, + routeState: routeState, + ); + } + + void _setupInitialStatefulShellRouteState() { + final List<_StatefulShellBranchState> states = _branches + .map((StatefulShellBranch e) => _createStatefulShellBranchState(e)) + .toList(); + + _routeState = _StatefulShellRouteState._( + widget.shellRoute, + states, + 0, + _switchActiveBranch, + ); + } + + @override + void initState() { + super.initState(); + _setupInitialStatefulShellRouteState(); + } + + @override + void didUpdateWidget(covariant _StatefulNavigationShell oldWidget) { + super.didUpdateWidget(oldWidget); + _updateRouteStateFromWidget(); + } + + @override + void didChangeDependencies() { + super.didChangeDependencies(); + _updateRouteStateFromWidget(); + } + + @override + Widget build(BuildContext context) { + final List children = _branches + .map((StatefulShellBranch branch) => _BranchNavigatorProxy( + branch: branch, navigatorForBranch: _navigatorForBranch)) + .toList(); + + return _InheritedStatefulNavigationShell( + routeState: _routeState, + child: Builder(builder: (BuildContext context) { + // This Builder Widget is mainly used to make it possible to access the + // StatefulShellRouteState via the BuildContext in the ShellRouteBuilder + final ShellBodyWidgetBuilder shellWidgetBuilder = + widget.shellBodyWidgetBuilder; + return shellWidgetBuilder( + context, + _currentGoRouterState, + _IndexedStackedRouteBranchContainer( + routeState: _routeState, children: children), + ); + }), + ); + } +} + +typedef _NavigatorForBranch = Widget? Function(StatefulShellBranch); + +/// Widget that serves as the proxy for a branch Navigator Widget, which +/// possibly hasn't been created yet. +class _BranchNavigatorProxy extends StatelessWidget { + const _BranchNavigatorProxy({ + required this.branch, + required this.navigatorForBranch, + }); + + final StatefulShellBranch branch; + final _NavigatorForBranch navigatorForBranch; + + @override + Widget build(BuildContext context) { + return navigatorForBranch(branch) ?? const SizedBox.shrink(); + } +} + +/// Default implementation of a container widget for the [Navigator]s of the +/// route branches. This implementation uses an [IndexedStack] as a container. +class _IndexedStackedRouteBranchContainer extends ShellNavigatorContainer { + const _IndexedStackedRouteBranchContainer( + {required this.routeState, required this.children}); + + final StatefulShellRouteState routeState; + + @override + final List children; + + @override + Widget build(BuildContext context) { + final int currentIndex = routeState.currentIndex; + final List stackItems = children + .mapIndexed((int index, Widget child) => + _buildRouteBranchContainer(context, currentIndex == index, child)) + .toList(); + + return IndexedStack(index: currentIndex, children: stackItems); + } + + Widget _buildRouteBranchContainer( + BuildContext context, bool isActive, Widget child) { + return Offstage( + offstage: !isActive, + child: TickerMode( + enabled: isActive, + child: child, + ), + ); + } +} + +extension _StatefulShellBranchStateHelper on StatefulShellBranchState { + GlobalKey get navigatorKey => branch.navigatorKey; +} + +typedef _BranchSwitcher = void Function(StatefulShellBranchState); + +/// The snapshot of the current state of a [StatefulShellRoute]. +/// +/// Note that this an immutable class, that represents the snapshot of the state +/// of a StatefulShellRoute at a given point in time. Therefore, instances of +/// this object should not be stored, but instead fetched fresh when needed, +/// using the method [StatefulShellRouteState.of]. +@immutable +class _StatefulShellRouteState implements StatefulShellRouteState { + /// Constructs a [StatefulShellRouteState]. + const _StatefulShellRouteState._( + this.route, + this._branchStates, + this.currentIndex, + _BranchSwitcher switchActiveBranch, + ) : _switchActiveBranch = switchActiveBranch; + + /// Constructs a copy of this [StatefulShellRouteState], with updated values + /// for some of the fields. + _StatefulShellRouteState _copy( + {List<_StatefulShellBranchState>? branchStates, int? currentIndex}) { + return _StatefulShellRouteState._( + route, + branchStates ?? _branchStates, + currentIndex ?? this.currentIndex, + _switchActiveBranch, + ); + } + + /// The associated [StatefulShellRoute] + @override + final StatefulShellRoute route; + + final List<_StatefulShellBranchState> _branchStates; + + /// The state for all separate route branches associated with a + /// [StatefulShellRoute]. + @override + List get branchStates => _branchStates; + + /// The state associated with the current [StatefulShellBranch]. + @override + StatefulShellBranchState get currentBranchState => branchStates[currentIndex]; + + /// The index of the currently active [StatefulShellBranch]. + /// + /// Corresponds to the index of the branch in the List returned from + /// branchBuilder of [StatefulShellRoute]. + @override + final int currentIndex; + + /// The Navigator key of the current navigator. + @override + GlobalKey get currentNavigatorKey => + currentBranchState.branch.navigatorKey; + + final _BranchSwitcher _switchActiveBranch; + + /// Navigate to the current location of the shell navigator with the provided + /// index. + /// + /// This method will switch the currently active [Navigator] for the + /// [StatefulShellRoute] by replacing the current navigation stack with the + /// one of the route branch identified by the provided index. If resetLocation + /// is true, the branch will be reset to its initial location + /// (see [StatefulShellBranch.initialLocation]). + @override + void goBranch({ + required int index, + }) { + _switchActiveBranch(branchStates[index]); + } + + @override + bool operator ==(Object other) { + if (identical(other, this)) { + return true; + } + if (other is! _StatefulShellRouteState) { + return false; + } + return other.route == route && + listEquals(other._branchStates, _branchStates) && + other.currentIndex == currentIndex; + } + + @override + int get hashCode => Object.hash(route, currentIndex, currentIndex); +} + +/// The snapshot of the current state for a particular route branch +/// ([StatefulShellBranch]) in a [StatefulShellRoute]. +/// +/// Note that this an immutable class, that represents the snapshot of the state +/// of a StatefulShellBranchState at a given point in time. Therefore, instances of +/// this object should not be stored, but instead fetched fresh when needed, +/// via the [StatefulShellRouteState] returned by the method +/// [StatefulShellRouteState.of]. +@immutable +class _StatefulShellBranchState implements StatefulShellBranchState { + /// Constructs a [StatefulShellBranchState]. + const _StatefulShellBranchState._({ + required this.branch, + this.isLoaded = false, + this.routeState, + }); + + /// Constructs a copy of this [StatefulShellBranchState], with updated values for + /// some of the fields. + _StatefulShellBranchState _copy({bool? isLoaded, GoRouterState? routeState}) { + return _StatefulShellBranchState._( + branch: branch, + isLoaded: isLoaded ?? this.isLoaded, + routeState: routeState ?? this.routeState, + ); + } + + /// The associated [StatefulShellBranch] + @override + final StatefulShellBranch branch; + + /// The current GoRouterState associated with the branch. + @override + final GoRouterState? routeState; + + /// Returns true if this branch has been loaded (i.e. visited once or + /// pre-loaded). + @override + final bool isLoaded; @override bool operator ==(Object other) { if (identical(other, this)) { return true; } - if (other is! StatefulShellBranch) { + if (other is! StatefulShellBranchState) { return false; } - return other.navigatorKey == navigatorKey && - other.initialLocation == initialLocation && - other.preload == preload && - other.restorationScopeId == restorationScopeId; + return other.branch == branch && other.routeState == routeState; } @override - int get hashCode => - Object.hash(navigatorKey, initialLocation, preload, restorationScopeId); + int get hashCode => Object.hash(branch, routeState); } diff --git a/packages/go_router/lib/src/shell_state.dart b/packages/go_router/lib/src/shell_state.dart index 3fcb8cd44f8c..83f68849525d 100644 --- a/packages/go_router/lib/src/shell_state.dart +++ b/packages/go_router/lib/src/shell_state.dart @@ -2,11 +2,10 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'package:flutter/foundation.dart'; import 'package:flutter/widgets.dart'; -import '../go_router.dart'; -import 'misc/stateful_navigation_shell.dart'; +import 'route.dart'; +import 'state.dart'; /// The snapshot of the current state of a [StatefulShellRoute]. /// @@ -15,57 +14,25 @@ import 'misc/stateful_navigation_shell.dart'; /// this object should not be stored, but instead fetched fresh when needed, /// using the method [StatefulShellRouteState.of]. @immutable -class StatefulShellRouteState { - /// Constructs a [StatefulShellRouteState]. - const StatefulShellRouteState({ - required this.route, - required this.branchStates, - required this.currentIndex, - required void Function(StatefulShellBranchState, bool) switchActiveBranch, - required void Function(StatefulShellBranchState? branchState, - bool navigateToDefaultLocation) - resetState, - }) : _switchActiveBranch = switchActiveBranch, - _resetState = resetState; - - /// Constructs a copy of this [StatefulShellRouteState], with updated values - /// for some of the fields. - StatefulShellRouteState copy( - {List? branchStates, int? currentIndex}) { - return StatefulShellRouteState( - route: route, - branchStates: branchStates ?? this.branchStates, - currentIndex: currentIndex ?? this.currentIndex, - switchActiveBranch: _switchActiveBranch, - resetState: _resetState, - ); - } - +abstract class StatefulShellRouteState { /// The associated [StatefulShellRoute] - final StatefulShellRoute route; + StatefulShellRoute get route; /// The state for all separate route branches associated with a /// [StatefulShellRoute]. - final List branchStates; + List get branchStates; /// The state associated with the current [StatefulShellBranch]. - StatefulShellBranchState get currentBranchState => branchStates[currentIndex]; + StatefulShellBranchState get currentBranchState; /// The index of the currently active [StatefulShellBranch]. /// /// Corresponds to the index of the branch in the List returned from /// branchBuilder of [StatefulShellRoute]. - final int currentIndex; + int get currentIndex; /// The Navigator key of the current navigator. - GlobalKey get currentNavigatorKey => - currentBranchState.branch.navigatorKey; - - final void Function(StatefulShellBranchState, bool) _switchActiveBranch; - - final void Function( - StatefulShellBranchState? branchState, bool navigateToDefaultLocation) - _resetState; + GlobalKey get currentNavigatorKey; /// Navigate to the current location of the shell navigator with the provided /// index. @@ -77,60 +44,11 @@ class StatefulShellRouteState { /// (see [StatefulShellBranch.initialLocation]). void goBranch({ required int index, - bool resetLocation = false, - }) { - _switchActiveBranch(branchStates[index], resetLocation); - } - - /// Refreshes this StatefulShellRouteState by rebuilding the state for the - /// current location. - void refresh() { - _switchActiveBranch(currentBranchState, true); - } - - /// Resets this StatefulShellRouteState by clearing all navigation state of - /// the branches - /// - /// After the state has been reset, the current branch will navigated to its - /// initial location, if [navigateToDefaultLocation] is true. - void reset({bool navigateToDefaultLocation = true}) { - _resetState(null, navigateToDefaultLocation); - } - - /// Resets the navigation state of the branch identified by the provided index. - /// - /// After the state has been reset, the branch will navigated to its - /// initial location, if [navigateToDefaultLocation] is true. - void resetBranch({ - required int index, - bool navigateToDefaultLocation = true, - }) { - _resetState(branchStates[index], navigateToDefaultLocation); - } - - @override - bool operator ==(Object other) { - if (identical(other, this)) { - return true; - } - if (other is! StatefulShellRouteState) { - return false; - } - return other.route == route && - listEquals(other.branchStates, branchStates) && - other.currentIndex == currentIndex; - } - - @override - int get hashCode => Object.hash(route, currentIndex, currentIndex); + }); /// Gets the state for the nearest stateful shell route in the Widget tree. static StatefulShellRouteState of(BuildContext context) { - final InheritedStatefulNavigationShell? inherited = context - .dependOnInheritedWidgetOfExactType(); - assert(inherited != null, - 'No InheritedStatefulNavigationShell found in context'); - return inherited!.routeState; + return StatefulShellRouteStateContext.of(context); } } @@ -143,50 +61,14 @@ class StatefulShellRouteState { /// via the [StatefulShellRouteState] returned by the method /// [StatefulShellRouteState.of]. @immutable -class StatefulShellBranchState { - /// Constructs a [StatefulShellBranchState]. - const StatefulShellBranchState({ - required this.branch, - this.isLoaded = false, - this.routeState, - }); - - /// Constructs a copy of this [StatefulShellBranchState], with updated values for - /// some of the fields. - StatefulShellBranchState copy({bool? isLoaded, GoRouterState? routeState}) { - return StatefulShellBranchState( - branch: branch, - isLoaded: isLoaded ?? this.isLoaded, - routeState: routeState ?? this.routeState, - ); - } - +abstract class StatefulShellBranchState { /// The associated [StatefulShellBranch] - final StatefulShellBranch branch; + StatefulShellBranch get branch; /// The current GoRouterState associated with the branch. - final GoRouterState? routeState; + GoRouterState? get routeState; /// Returns true if this branch has been loaded (i.e. visited once or /// pre-loaded). - final bool isLoaded; - - @override - bool operator ==(Object other) { - if (identical(other, this)) { - return true; - } - if (other is! StatefulShellBranchState) { - return false; - } - return other.branch == branch && other.routeState == routeState; - } - - @override - int get hashCode => Object.hash(branch, routeState); - - /// Gets the state for the current branch of the nearest stateful shell route - /// in the Widget tree. - static StatefulShellBranchState of(BuildContext context) => - StatefulShellRouteState.of(context).currentBranchState; + bool get isLoaded; } diff --git a/packages/go_router/lib/src/state.dart b/packages/go_router/lib/src/state.dart index 06341653077e..e8322dbe2a74 100644 --- a/packages/go_router/lib/src/state.dart +++ b/packages/go_router/lib/src/state.dart @@ -235,8 +235,7 @@ class GoRouterStateRegistry extends ChangeNotifier { } /// Updates this registry with new records. - void updateRegistry(Map, GoRouterState> newRegistry, - {bool replace = true}) { + void updateRegistry(Map, GoRouterState> newRegistry) { bool shouldNotify = false; final Set> pagesWithAssociation = _routePageAssociation.values.toSet(); @@ -256,21 +255,19 @@ class GoRouterStateRegistry extends ChangeNotifier { // Adding or removing registry does not need to notify the listen since // no one should be depending on them. } - if (replace) { - registry.removeWhere((Page key, GoRouterState value) { - if (newRegistry.containsKey(key)) { - return false; - } - // For those that have page route association, it will be removed by the - // route future. Need to notify the listener so they can update the page - // route association if its page has changed. - if (pagesWithAssociation.contains(key)) { - shouldNotify = true; - return false; - } - return true; - }); - } + registry.removeWhere((Page key, GoRouterState value) { + if (newRegistry.containsKey(key)) { + return false; + } + // For those that have page route association, it will be removed by the + // route future. Need to notify the listener so they can update the page + // route association if its page has changed. + if (pagesWithAssociation.contains(key)) { + shouldNotify = true; + return false; + } + return true; + }); if (shouldNotify) { notifyListeners(); } diff --git a/packages/go_router/test/go_router_test.dart b/packages/go_router/test/go_router_test.dart index 96eba75c8859..0a527c918619 100644 --- a/packages/go_router/test/go_router_test.dart +++ b/packages/go_router/test/go_router_test.dart @@ -3089,7 +3089,7 @@ void main() { await tester.pumpAndSettle(); expect(statefulWidgetKey.currentState?.counter, equals(1)); - routeState!.goBranch(index: 0, resetLocation: true); + router.go('/a'); await tester.pumpAndSettle(); expect(find.text('Screen A'), findsOneWidget); expect(find.text('Screen A Detail'), findsNothing); @@ -3510,144 +3510,6 @@ void main() { expect(find.text('Screen B Detail2'), findsOneWidget); expect(find.text('Screen C2'), findsNothing); }); - - testWidgets('StatefulShellRoute is correctly reset', - (WidgetTester tester) async { - final GlobalKey rootNavigatorKey = - GlobalKey(); - StatefulShellRouteState? routeState; - - final List routes = [ - StatefulShellRoute( - builder: (StatefulShellBuilder shellBuilder) { - return shellBuilder.buildShell( - (BuildContext context, GoRouterState state, Widget child) { - routeState = StatefulShellRouteState.of(context); - return child; - }); - }, - branches: [ - StatefulShellBranch(routes: [ - GoRoute( - path: '/a', - builder: (BuildContext context, GoRouterState state) => - const Text('Screen A'), - routes: [ - GoRoute( - path: 'detail', - builder: (BuildContext context, GoRouterState state) => - const Text('Screen A Detail'), - ), - ], - ), - ]), - StatefulShellBranch(routes: [ - GoRoute( - path: '/b', - builder: (BuildContext context, GoRouterState state) => - const Text('Screen B'), - routes: [ - GoRoute( - path: 'detail', - builder: (BuildContext context, GoRouterState state) => - const Text('Screen B Detail'), - ), - ], - ), - ]), - ], - ), - ]; - - final GoRouter router = await createRouter(routes, tester, - initialLocation: '/a/detail', navigatorKey: rootNavigatorKey); - expect(find.text('Screen A'), findsNothing); - expect(find.text('Screen A Detail'), findsOneWidget); - - router.go('/b/detail'); - await tester.pumpAndSettle(); - expect(find.text('Screen B'), findsNothing); - expect(find.text('Screen B Detail'), findsOneWidget); - - routeState!.reset(); - await tester.pumpAndSettle(); - expect(find.text('Screen B'), findsOneWidget); - expect(find.text('Screen B Detail'), findsNothing); - - routeState!.goBranch(index: 0); - await tester.pumpAndSettle(); - expect(find.text('Screen A'), findsOneWidget); - expect(find.text('Screen A Detail'), findsNothing); - }); - - testWidgets('Single branch of StatefulShellRoute is correctly reset', - (WidgetTester tester) async { - final GlobalKey rootNavigatorKey = - GlobalKey(); - StatefulShellRouteState? routeState; - - final List routes = [ - StatefulShellRoute( - builder: (StatefulShellBuilder shellBuilder) { - return shellBuilder.buildShell( - (BuildContext context, GoRouterState state, Widget child) { - routeState = StatefulShellRouteState.of(context); - return child; - }); - }, - branches: [ - StatefulShellBranch(routes: [ - GoRoute( - path: '/a', - builder: (BuildContext context, GoRouterState state) => - const Text('Screen A'), - routes: [ - GoRoute( - path: 'detail', - builder: (BuildContext context, GoRouterState state) => - const Text('Screen A Detail'), - ), - ], - ), - ]), - StatefulShellBranch(routes: [ - GoRoute( - path: '/b', - builder: (BuildContext context, GoRouterState state) => - const Text('Screen B'), - routes: [ - GoRoute( - path: 'detail', - builder: (BuildContext context, GoRouterState state) => - const Text('Screen B Detail'), - ), - ], - ), - ]), - ], - ), - ]; - - final GoRouter router = await createRouter(routes, tester, - initialLocation: '/a/detail', navigatorKey: rootNavigatorKey); - expect(find.text('Screen A'), findsNothing); - expect(find.text('Screen A Detail'), findsOneWidget); - - router.go('/b/detail'); - await tester.pumpAndSettle(); - expect(find.text('Screen B'), findsNothing); - expect(find.text('Screen B Detail'), findsOneWidget); - - routeState!.resetBranch(index: 1); - await tester.pumpAndSettle(); - expect(find.text('Screen B'), findsOneWidget); - expect(find.text('Screen B Detail'), findsNothing); - - routeState!.goBranch(index: 0); - await tester.pumpAndSettle(); - expect(find.text('Screen A'), findsNothing); - expect(find.text('Screen A Detail'), findsOneWidget); - }); }); group('Imperative navigation', () { From ee047c814bdeae670978a7dd516884418fca7768 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20Lo=CC=88fstrand?= Date: Wed, 8 Mar 2023 13:42:10 +0100 Subject: [PATCH 088/112] Refactoring of StatefulShellRoute and related classes to simplify builder/pageBuilder API (removed InheritedWidget and changed how StatefulShellRouteState is created and used). --- .../example/lib/stateful_shell_route.dart | 89 ++- packages/go_router/lib/go_router.dart | 7 +- packages/go_router/lib/src/builder.dart | 241 +++--- packages/go_router/lib/src/configuration.dart | 1 - packages/go_router/lib/src/delegate.dart | 21 +- packages/go_router/lib/src/matching.dart | 73 +- packages/go_router/lib/src/redirection.dart | 2 - packages/go_router/lib/src/route.dart | 708 ++++++++---------- packages/go_router/lib/src/router.dart | 8 - packages/go_router/lib/src/shell_state.dart | 74 -- packages/go_router/lib/src/state.dart | 44 +- packages/go_router/lib/src/typedefs.dart | 15 +- packages/go_router/test/builder_test.dart | 11 +- packages/go_router/test/go_router_test.dart | 70 +- packages/go_router/test/test_helpers.dart | 7 +- 15 files changed, 529 insertions(+), 842 deletions(-) delete mode 100644 packages/go_router/lib/src/shell_state.dart diff --git a/packages/go_router/example/lib/stateful_shell_route.dart b/packages/go_router/example/lib/stateful_shell_route.dart index 3e56bd4f7965..d5796febfdb0 100644 --- a/packages/go_router/example/lib/stateful_shell_route.dart +++ b/packages/go_router/example/lib/stateful_shell_route.dart @@ -153,43 +153,44 @@ class NestedTabNavigationExampleApp extends StatelessWidget { ), ]), ], - builder: (StatefulShellBuilder shellBuilder) { - /// For this nested StatefulShellRoute, a custom container - /// (TabBarView) is used for the branch navigators, and thus - /// ignoring the default navigator container passed to the - /// builder. Instead, the branch navigators are passed - /// directly to the TabbedRootScreen, using the children - /// field of ShellNavigatorContainer. See TabbedRootScreen - /// for more details on how the children are used in the - /// TabBarView. - return shellBuilder.buildShell( - (BuildContext context, GoRouterState state, - ShellNavigatorContainer child) => - TabbedRootScreen(children: child.children), + builder: (BuildContext context, StatefulShellRouteState state, + Widget child) { + /// This nested StatefulShellRoute demonstrates the use of a + /// custom container (TabBarView) for the branch Navigators. + /// Here, the default branch Navigator container (`child`) is + /// ignored, and the class StatefulShellRouteController is + /// instead used to provide access to the widgets representing + /// the branch Navigators (`List children`). + /// + /// See TabbedRootScreen for more details on how the children + /// are used in the TabBarView. + return StatefulShellRouteController( + shellRouteState: state, + containerBuilder: (BuildContext context, + StatefulShellRouteState state, + List children) => + TabbedRootScreen(shellState: state, children: children), ); }, ), ], ), ], - builder: (StatefulShellBuilder shellBuilder) { - /// This builder implementation uses the default navigator container - /// (ShellNavigatorContainer) to host the branch navigators. This is - /// the simplest way to use StatefulShellRoute, when no separate - /// customization is needed for the branch Widgets (Navigators). - return shellBuilder.buildShell((BuildContext context, - GoRouterState state, ShellNavigatorContainer child) => - ScaffoldWithNavBar(body: child)); + builder: (BuildContext context, StatefulShellRouteState state, + Widget child) { + /// This builder implementation uses the default container for the + /// branch Navigators (provided in through the `child` argument). This + /// is the simplest way to use StatefulShellRoute, where the shell is + /// built around the Navigator container (see ScaffoldWithNavBar). + return ScaffoldWithNavBar(shellState: state, body: child); }, /// If it's necessary to customize the Page for StatefulShellRoute, /// provide a pageBuilder function instead of the builder, for example: - // pageBuilder: (StatefulShellBuilder shellBuilder) { - // final Widget statefulShell = shellBuilder.buildShell( - // (BuildContext context, GoRouterState state, - // ShellNavigatorContainer child) => - // ScaffoldWithNavBar(body: child)); - // return NoTransitionPage(child: statefulShell); + // pageBuilder: (BuildContext context, StatefulShellRouteState state, + // Widget child) { + // return NoTransitionPage( + // child: ScaffoldWithNavBar(shellState: state, body: child)); // }, ), ], @@ -212,18 +213,19 @@ class NestedTabNavigationExampleApp extends StatelessWidget { class ScaffoldWithNavBar extends StatelessWidget { /// Constructs an [ScaffoldWithNavBar]. const ScaffoldWithNavBar({ + required this.shellState, required this.body, Key? key, }) : super(key: key ?? const ValueKey('ScaffoldWithNavBar')); - /// Body, i.e. the index stack + /// The current state of the parent StatefulShellRoute. + final StatefulShellRouteState shellState; + + /// Body, i.e. the container for the branch Navigators. final Widget body; @override Widget build(BuildContext context) { - final StatefulShellRouteState shellState = - StatefulShellRouteState.of(context); - return Scaffold( body: body, bottomNavigationBar: BottomNavigationBar( @@ -290,7 +292,7 @@ class RootScreen extends StatelessWidget { onPressed: () { GoRouter.of(context).push('/modal'); }, - child: const Text('Show modal screen on root navigator'), + child: const Text('Show modal screen on ROOT navigator'), ), const Padding(padding: EdgeInsets.all(4)), ElevatedButton( @@ -300,7 +302,17 @@ class RootScreen extends StatelessWidget { useRootNavigator: true, builder: _bottomSheet); }, - child: const Text('Show bottom sheet on root navigator'), + child: const Text('Show bottom sheet on ROOT navigator'), + ), + const Padding(padding: EdgeInsets.all(4)), + ElevatedButton( + onPressed: () { + showModalBottomSheet( + context: context, + useRootNavigator: false, + builder: _bottomSheet); + }, + child: const Text('Show bottom sheet on CURRENT navigator'), ), ], ), @@ -450,15 +462,18 @@ class ModalScreen extends StatelessWidget { /// Builds a nested shell using a [TabBar] and [TabBarView]. class TabbedRootScreen extends StatelessWidget { /// Constructs a TabbedRootScreen - const TabbedRootScreen({required this.children, Key? key}) : super(key: key); + const TabbedRootScreen( + {required this.shellState, required this.children, Key? key}) + : super(key: key); + + /// The current state of the parent StatefulShellRoute. + final StatefulShellRouteState shellState; /// The children (Navigators) to display in the [TabBarView]. final List children; @override Widget build(BuildContext context) { - final StatefulShellRouteState shellState = - StatefulShellRouteState.of(context); final List tabs = children.mapIndexed((int i, _) => Tab(text: 'Tab ${i + 1}')).toList(); @@ -480,7 +495,7 @@ class TabbedRootScreen extends StatelessWidget { } void _onTabTap(BuildContext context, int index) { - StatefulShellRouteState.of(context).goBranch(index: index); + shellState.goBranch(index: index); } } diff --git a/packages/go_router/lib/go_router.dart b/packages/go_router/lib/go_router.dart index 920ad996c542..c47e436c9731 100644 --- a/packages/go_router/lib/go_router.dart +++ b/packages/go_router/lib/go_router.dart @@ -11,13 +11,11 @@ export 'src/configuration.dart' GoRoute, GoRouterState, RouteBase, - ShellNavigatorBuilder, - ShellNavigatorContainer, ShellRoute, + ShellNavigatorContainerBuilder, StatefulShellBranch, - StatefulShellBranchState, - StatefulShellBuilder, StatefulShellRoute, + StatefulShellRouteController, StatefulShellRouteState; export 'src/misc/extensions.dart'; export 'src/misc/inherited_router.dart'; @@ -31,6 +29,5 @@ export 'src/typedefs.dart' GoRouterWidgetBuilder, ShellRouteBuilder, ShellRoutePageBuilder, - ShellBodyWidgetBuilder, StatefulShellRouteBuilder, StatefulShellRoutePageBuilder; diff --git a/packages/go_router/lib/src/builder.dart b/packages/go_router/lib/src/builder.dart index 88782c204457..0cea7b012c29 100644 --- a/packages/go_router/lib/src/builder.dart +++ b/packages/go_router/lib/src/builder.dart @@ -14,7 +14,6 @@ import 'matching.dart'; import 'misc/error_screen.dart'; import 'pages/cupertino.dart'; import 'pages/material.dart'; -import 'parser.dart'; import 'route_data.dart'; import 'typedefs.dart'; @@ -35,6 +34,7 @@ class RouteBuilder { required this.errorBuilder, required this.restorationScopeId, required this.observers, + required this.onPopPage, }); /// Builder function for a go router with Navigator. @@ -57,6 +57,10 @@ class RouteBuilder { /// changes. final List observers; + /// Function used as [Navigator.onPopPage] callback, that additionally + /// provides the [RouteMatch] associated with the popped Page. + final RouteBuilderPopPageCallback onPopPage; + final GoRouterStateRegistry _registry = GoRouterStateRegistry(); /// Caches a HeroController for the nested Navigator, which solves cases where the @@ -68,7 +72,6 @@ class RouteBuilder { Widget build( BuildContext context, RouteMatchList matchList, - RouteBuilderPopPageCallback onPopPage, bool routerNeglect, ) { if (matchList.isEmpty) { @@ -83,8 +86,8 @@ class RouteBuilder { try { final Map, GoRouterState> newRegistry = , GoRouterState>{}; - final Widget result = tryBuild(context, matchList, onPopPage, - routerNeglect, configuration.navigatorKey, newRegistry); + final Widget result = tryBuild(context, matchList, routerNeglect, + configuration.navigatorKey, newRegistry); _registry.updateRegistry(newRegistry); return GoRouterStateRegistryScope( registry: _registry, child: result); @@ -105,7 +108,6 @@ class RouteBuilder { Widget tryBuild( BuildContext context, RouteMatchList matchList, - RouteBuilderPopPageCallback onPopPage, bool routerNeglect, GlobalKey navigatorKey, Map, GoRouterState> registry, @@ -113,30 +115,19 @@ class RouteBuilder { final _PagePopContext pagePopContext = _PagePopContext._(onPopPage); return builderWithNav( context, - _RouteNavigatorBuilder.buildNavigator( + _buildNavigator( pagePopContext.onPopPage, _buildPages(context, matchList, 0, pagePopContext, routerNeglect, navigatorKey, registry), navigatorKey, observers: observers, + restorationScopeId: restorationScopeId, ), ); } /// Returns the top-level pages instead of the root navigator. Used for /// testing. - @visibleForTesting - List> buildPages( - BuildContext context, - RouteMatchList matchList, - RouteBuilderPopPageCallback onPopPage, - int startIndex, - bool routerNeglect, - GlobalKey navigatorKey, - Map, GoRouterState> registry) => - _buildPages(context, matchList, startIndex, _PagePopContext._(onPopPage), - routerNeglect, navigatorKey, registry); - List> _buildPages( BuildContext context, RouteMatchList matchList, @@ -148,8 +139,8 @@ class RouteBuilder { final Map, List>> keyToPage = , List>>{}; try { - _buildRecursive(context, matchList.unmodifiableMatchList(), startIndex, - pagePopContext, routerNeglect, keyToPage, navigatorKey, registry); + _buildRecursive(context, matchList, startIndex, pagePopContext, + routerNeglect, keyToPage, navigatorKey, registry); // Every Page should have a corresponding RouteMatch. assert(keyToPage.values.flattened.every((Page page) => @@ -172,7 +163,6 @@ class RouteBuilder { BuildContext context, RouteMatchList matchList, int startIndex, - RouteBuilderPopPageCallback onPopPage, bool routerNeglect, GlobalKey navigatorKey, {List? observers, @@ -183,14 +173,14 @@ class RouteBuilder { final _PagePopContext pagePopContext = _PagePopContext._(onPopPage); _buildRecursive( context, - matchList.unmodifiableMatchList(), + matchList, startIndex, pagePopContext, routerNeglect, keyToPage, navigatorKey, , GoRouterState>{}); - return _RouteNavigatorBuilder.buildNavigator( + return _buildNavigator( pagePopContext.onPopPage, keyToPage[navigatorKey]!, navigatorKey, @@ -206,7 +196,7 @@ class RouteBuilder { void _buildRecursive( BuildContext context, - UnmodifiableRouteMatchList matchList, + RouteMatchList matchList, int startIndex, _PagePopContext pagePopContext, bool routerNeglect, @@ -270,20 +260,27 @@ class RouteBuilder { final HeroController heroController = _goHeroCache.putIfAbsent( shellNavigatorKey, () => _getHeroController(context)); - // Build the Navigator builder for this shell route - final _RouteNavigatorBuilder navigatorBuilder = _RouteNavigatorBuilder._( - this, - state, - route, - heroController, - shellNavigatorKey, + // Build the Navigator for this shell route + Widget buildShellNavigator( + List? observers, String? restorationScopeId) { + return _buildNavigator( + pagePopContext.onPopPage, keyToPages[shellNavigatorKey]!, - pagePopContext); + shellNavigatorKey, + observers: observers, + restorationScopeId: restorationScopeId, + heroController: heroController, + ); + } + + final _ShellNavigatorBuilder shellNavigatorBuilder = + _ShellNavigatorBuilder._( + this, matchList, shellNavigatorKey, buildShellNavigator); // Build the Page for this route final Page page = _buildPageForRoute( context, state, match, pagePopContext, - child: navigatorBuilder); + shellNavigatorBuilder: shellNavigatorBuilder); registry[page] = state; // Place the ShellRoute's Page onto the list for the parent navigator. keyToPages @@ -292,11 +289,35 @@ class RouteBuilder { } } + static Widget _buildNavigator( + PopPageCallback onPopPage, + List> pages, + Key? navigatorKey, { + List? observers, + String? restorationScopeId, + HeroController? heroController, + }) { + final Widget navigator = Navigator( + key: navigatorKey, + restorationScopeId: restorationScopeId, + pages: pages, + observers: observers ?? const [], + onPopPage: onPopPage, + ); + if (heroController != null) { + return HeroControllerScope( + controller: heroController, + child: navigator, + ); + } else { + return navigator; + } + } + /// Helper method that builds a [GoRouterState] object for the given [match] /// and [params]. @visibleForTesting - GoRouterState buildState( - UnmodifiableRouteMatchList matchList, RouteMatch match) { + GoRouterState buildState(RouteMatchList matchList, RouteMatch match) { final RouteBase route = match.route; String? name; String path = ''; @@ -308,7 +329,6 @@ class RouteBuilder { match is ImperativeRouteMatch ? match.matches : matchList; final GoRouterState state = GoRouterState( configuration, - matchList, location: effectiveMatchList.uri.toString(), subloc: match.subloc, name: name, @@ -327,7 +347,7 @@ class RouteBuilder { /// Builds a [Page] for [StackedRoute] Page _buildPageForRoute(BuildContext context, GoRouterState state, RouteMatch match, _PagePopContext pagePopContext, - {Object? child}) { + {ShellNavigatorBuilder? shellNavigatorBuilder}) { final RouteBase route = match.route; Page? page; @@ -338,9 +358,9 @@ class RouteBuilder { page = pageBuilder(context, state); } } else if (route is ShellRouteBase) { - assert(child is ShellNavigatorBuilder, - '${route.runtimeType} must contain a child'); - page = route.buildPage(context, state, child! as ShellNavigatorBuilder); + assert(shellNavigatorBuilder != null, + 'ShellNavigatorBuilder must be provided for ${route.runtimeType}'); + page = route.buildPage(context, state, shellNavigatorBuilder!); } if (page is NoOpPage) { @@ -348,7 +368,8 @@ class RouteBuilder { } page ??= buildPage(context, state, Builder(builder: (BuildContext context) { - return _callRouteBuilder(context, state, match, child: child); + return _callRouteBuilder(context, state, match, + shellNavigatorBuilder: shellNavigatorBuilder); })); pagePopContext._setRouteMatchForPage(page, match); @@ -359,7 +380,7 @@ class RouteBuilder { /// Calls the user-provided route builder from the [RouteMatch]'s [RouteBase]. Widget _callRouteBuilder( BuildContext context, GoRouterState state, RouteMatch match, - {Object? child}) { + {ShellNavigatorBuilder? shellNavigatorBuilder}) { final RouteBase route = match.route; if (route is GoRoute) { @@ -371,12 +392,10 @@ class RouteBuilder { return builder(context, state); } else if (route is ShellRouteBase) { - if (child is! ShellNavigatorBuilder) { - throw _RouteBuilderException( - 'Attempt to build ShellRoute without a child widget'); - } - - final Widget? widget = route.buildWidget(context, state, child); + assert(shellNavigatorBuilder != null, + 'ShellNavigatorBuilder must be provided for ${route.runtimeType}'); + final Widget? widget = + route.buildWidget(context, state, shellNavigatorBuilder!); if (widget == null) { throw _RouteBuilderError('No builder provided to ShellRoute: $route'); } @@ -465,7 +484,7 @@ class RouteBuilder { RouteMatchList matchList, RouteBuilderPopPageCallback onPopPage, GlobalKey navigatorKey) { - return _RouteNavigatorBuilder.buildNavigator( + return _buildNavigator( (Route route, dynamic result) => onPopPage(route, result, null), >[ _buildErrorPage(context, e, matchList), @@ -483,7 +502,6 @@ class RouteBuilder { final Uri uri = matchList.uri; final GoRouterState state = GoRouterState( configuration, - matchList.unmodifiableMatchList(), location: uri.toString(), subloc: uri.path, name: null, @@ -598,128 +616,37 @@ class _PagePopContext { } } +typedef _NavigatorBuilder = Widget Function( + List? observers, String? restorationScopeId); + /// Provides support for building Navigators for routes. -class _RouteNavigatorBuilder extends ShellNavigatorBuilder { - /// Constructs a NavigatorBuilder. - _RouteNavigatorBuilder._( +class _ShellNavigatorBuilder extends ShellNavigatorBuilder { + _ShellNavigatorBuilder._( this.routeBuilder, - this.state, - this.currentRoute, - this.heroController, - this.navigatorKeyForCurrentRoute, - this.pages, - this.pagePopContext, + this.routeMatchList, + this.navigatorKey, + this.navigatorBuilder, ); - /// The route builder. + @override final RouteBuilder routeBuilder; @override - final GoRouterState state; + final RouteMatchList routeMatchList; @override - final ShellRouteBase currentRoute; + final GlobalKey navigatorKey; - /// The hero controller. - final HeroController heroController; + final _NavigatorBuilder navigatorBuilder; @override - final GlobalKey navigatorKeyForCurrentRoute; - - /// The pages for the current route. - final List> pages; - - /// The page pop context. - final _PagePopContext pagePopContext; - - /// Builds a navigator. - static Widget buildNavigator( - PopPageCallback onPopPage, - List> pages, - Key? navigatorKey, { - List? observers, - String? restorationScopeId, - HeroController? heroController, - }) { - final Widget navigator = Navigator( - key: navigatorKey, - restorationScopeId: restorationScopeId, - pages: pages, - observers: observers ?? const [], - onPopPage: onPopPage, - ); - if (heroController != null) { - return HeroControllerScope( - controller: heroController, - child: navigator, - ); - } else { - return navigator; - } - } - - @override - Widget buildNavigatorForCurrentRoute({ - List? observers, - String? restorationScopeId, - GlobalKey? navigatorKey, - }) { - return buildNavigator( - pagePopContext.onPopPage, - pages, - navigatorKey ?? navigatorKeyForCurrentRoute, - observers: observers, - restorationScopeId: restorationScopeId, - heroController: heroController, - ); - } - - @override - Future buildPreloadedShellNavigator({ - required BuildContext context, - required String location, - required GlobalKey navigatorKey, - required ShellRouteBase parentShellRoute, + Widget buildNavigator({ List? observers, String? restorationScopeId, }) { - // Parse a RouteMatchList from location and handle any redirects - final GoRouteInformationParser parser = - GoRouter.of(context).routeInformationParser; - final Future routeMatchList = - parser.parseRouteInformationWithDependencies( - RouteInformation(location: location), - context, + return navigatorBuilder( + observers, + restorationScopeId, ); - - Widget? buildNavigator(RouteMatchList matchList) { - // Find the index of fromRoute in the match list - final int parentShellRouteIndex = matchList.matches - .indexWhere((RouteMatch e) => e.route == parentShellRoute); - if (parentShellRouteIndex >= 0) { - final int startIndex = parentShellRouteIndex + 1; - final GlobalKey routeNavigatorKey = parentShellRoute - .navigatorKeyForSubRoute(matchList.matches[startIndex].route); - assert( - navigatorKey == routeNavigatorKey, - 'Incorrect shell navigator key ' - 'for preloaded match list for location "$location"'); - - return routeBuilder.buildPreloadedNestedNavigator( - context, - matchList, - parentShellRouteIndex + 1, - pagePopContext.routeBuilderOnPopPage, - true, - navigatorKey, - restorationScopeId: restorationScopeId, - observers: observers, - ); - } else { - return null; - } - } - - return routeMatchList.then(buildNavigator); } } diff --git a/packages/go_router/lib/src/configuration.dart b/packages/go_router/lib/src/configuration.dart index b16f35930ed7..1f13e12155af 100644 --- a/packages/go_router/lib/src/configuration.dart +++ b/packages/go_router/lib/src/configuration.dart @@ -12,7 +12,6 @@ import 'misc/errors.dart'; import 'path_utils.dart'; import 'typedefs.dart'; export 'route.dart'; -export 'shell_state.dart'; export 'state.dart'; /// The route configuration for GoRouter configured by the app. diff --git a/packages/go_router/lib/src/delegate.dart b/packages/go_router/lib/src/delegate.dart index e89dac719efc..59cb5cf40aeb 100644 --- a/packages/go_router/lib/src/delegate.dart +++ b/packages/go_router/lib/src/delegate.dart @@ -27,15 +27,17 @@ class GoRouterDelegate extends RouterDelegate required List observers, required this.routerNeglect, String? restorationScopeId, - }) : _configuration = configuration, - builder = RouteBuilder( - configuration: configuration, - builderWithNav: builderWithNav, - errorPageBuilder: errorPageBuilder, - errorBuilder: errorBuilder, - restorationScopeId: restorationScopeId, - observers: observers, - ); + }) : _configuration = configuration { + builder = RouteBuilder( + configuration: configuration, + builderWithNav: builderWithNav, + errorPageBuilder: errorPageBuilder, + errorBuilder: errorBuilder, + restorationScopeId: restorationScopeId, + observers: observers, + onPopPage: _onPopPage, + ); + } /// Builds the top-level Navigator given a configuration and location. @visibleForTesting @@ -170,7 +172,6 @@ class GoRouterDelegate extends RouterDelegate return builder.build( context, _matchList, - _onPopPage, routerNeglect, ); } diff --git a/packages/go_router/lib/src/matching.dart b/packages/go_router/lib/src/matching.dart index 59674176c478..6577518aa4bc 100644 --- a/packages/go_router/lib/src/matching.dart +++ b/packages/go_router/lib/src/matching.dart @@ -148,64 +148,26 @@ class RouteMatchList { } } -/// Unmodifiable version of [RouteMatchList]. +/// Unmodifiable version of [RouteMatchList] that also supports equality +/// checking based on data. @immutable -class UnmodifiableRouteMatchList implements RouteMatchList { +class UnmodifiableRouteMatchList { /// UnmodifiableRouteMatchList constructor. UnmodifiableRouteMatchList.from(RouteMatchList routeMatchList) : _matches = List.unmodifiable(routeMatchList.matches), - __uri = routeMatchList.uri, - fullpath = routeMatchList.fullpath, - pathParameters = - Map.unmodifiable(routeMatchList.pathParameters), - error = routeMatchList.error, - extra = routeMatchList.extra, - isEmpty = routeMatchList.isEmpty, - isError = routeMatchList.isError, - isNotEmpty = routeMatchList.isNotEmpty; + _uri = routeMatchList.uri, + _pathParameters = + Map.unmodifiable(routeMatchList.pathParameters); + + final List _matches; + final Uri _uri; + final Map _pathParameters; /// Creates a new [RouteMatchList] from this UnmodifiableRouteMatchList. RouteMatchList get modifiableMatchList => RouteMatchList( List.from(_matches), _uri, - Map.from(pathParameters)); - - final Uri __uri; - @override - Uri get uri => __uri; - @override - Uri get _uri => __uri; - @override - set _uri(Uri uri) => throw UnimplementedError(); - - @override - final List _matches; - @override - List get matches => _matches; - - @override - final String fullpath; - - @override - final Map pathParameters; - - @override - final Exception? error; - - @override - final Object? extra; - - @override - final bool isEmpty; - - @override - final bool isError; - - @override - final bool isNotEmpty; - - @override - RouteMatch get last => _matches.last; + Map.from(_pathParameters)); @override bool operator ==(Object other) { @@ -217,20 +179,11 @@ class UnmodifiableRouteMatchList implements RouteMatchList { } return listEquals(other._matches, _matches) && other._uri == _uri && - mapEquals(other.pathParameters, pathParameters); + mapEquals(other._pathParameters, _pathParameters); } @override - int get hashCode => Object.hash(_matches, _uri, pathParameters); - - @override - void push(RouteMatch match) => throw UnimplementedError(); - - @override - void remove(RouteMatch match) => throw UnimplementedError(); - - @override - UnmodifiableRouteMatchList unmodifiableMatchList() => this; + int get hashCode => Object.hash(_matches, _uri, _pathParameters); } /// An error that occurred during matching. diff --git a/packages/go_router/lib/src/redirection.dart b/packages/go_router/lib/src/redirection.dart index ba2cd3c91fb1..3ebef5cf294b 100644 --- a/packages/go_router/lib/src/redirection.dart +++ b/packages/go_router/lib/src/redirection.dart @@ -92,7 +92,6 @@ FutureOr redirect( context, GoRouterState( configuration, - prevMatchList.unmodifiableMatchList(), location: prevLocation, name: null, // No name available at the top level trim the query params off the @@ -138,7 +137,6 @@ FutureOr _getRouteLevelRedirect( context, GoRouterState( configuration, - matchList.unmodifiableMatchList(), location: matchList.uri.toString(), subloc: match.subloc, name: route.name, diff --git a/packages/go_router/lib/src/route.dart b/packages/go_router/lib/src/route.dart index f54bca84b50d..bafa1c4da0f8 100644 --- a/packages/go_router/lib/src/route.dart +++ b/packages/go_router/lib/src/route.dart @@ -2,12 +2,17 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'dart:async'; + import 'package:collection/collection.dart'; -import 'package:flutter/foundation.dart'; import 'package:flutter/widgets.dart'; import 'package:meta/meta.dart'; import '../go_router.dart'; +import 'builder.dart'; +import 'match.dart'; +import 'matching.dart'; +import 'parser.dart'; import 'path_utils.dart'; /// The base class for [GoRoute] and [ShellRoute]. @@ -335,7 +340,7 @@ abstract class ShellRouteBase extends RouteBase { /// Attempts to build the Page representing this shell route. /// - /// Returns null if this shell route does not build a Page, , but instead uses + /// 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, ShellNavigatorBuilder navigatorBuilder); @@ -348,27 +353,16 @@ abstract class ShellRouteBase extends RouteBase { /// Navigator builder for shell routes. abstract class ShellNavigatorBuilder { /// The [GlobalKey] to be used by the [Navigator] built for the current route. - GlobalKey get navigatorKeyForCurrentRoute; - - /// The current route state. - GoRouterState get state; + GlobalKey get navigatorKey; - /// The current shell route. - ShellRouteBase get currentRoute; + /// The route match list for the current route. + RouteMatchList get routeMatchList; - /// Builds a [Navigator] for the current route. - Widget buildNavigatorForCurrentRoute({ - List? observers, - String? restorationScopeId, - GlobalKey? navigatorKey, - }); + /// The route builder. + RouteBuilder get routeBuilder; - /// Builds a preloaded [Navigator] for a specific location. - Future buildPreloadedShellNavigator({ - required BuildContext context, - required String location, - required GlobalKey navigatorKey, - required ShellRouteBase parentShellRoute, + /// Builds the [Navigator] for the current route. + Widget buildNavigator({ List? observers, String? restorationScopeId, }); @@ -491,7 +485,7 @@ class ShellRoute extends ShellRouteBase { /// The widget builder for a shell route. /// - /// Similar to GoRoute builder, but with an additional child parameter. This + /// 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. @@ -499,7 +493,7 @@ class ShellRoute extends ShellRouteBase { /// The page builder for a shell route. /// - /// Similar to GoRoute pageBuilder, but with an additional child parameter. + /// 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. @@ -509,8 +503,8 @@ class ShellRoute extends ShellRouteBase { Widget? buildWidget(BuildContext context, GoRouterState state, ShellNavigatorBuilder navigatorBuilder) { if (builder != null) { - final Widget navigator = navigatorBuilder.buildNavigatorForCurrentRoute( - restorationScopeId: restorationScopeId, observers: observers); + final Widget navigator = navigatorBuilder.buildNavigator( + observers: observers, restorationScopeId: restorationScopeId); return builder!(context, state, navigator); } return null; @@ -520,8 +514,8 @@ class ShellRoute extends ShellRouteBase { Page? buildPage(BuildContext context, GoRouterState state, ShellNavigatorBuilder navigatorBuilder) { if (pageBuilder != null) { - final Widget navigator = navigatorBuilder.buildNavigatorForCurrentRoute( - restorationScopeId: restorationScopeId, observers: observers); + final Widget navigator = navigatorBuilder.buildNavigator( + observers: observers, restorationScopeId: restorationScopeId); return pageBuilder!(context, state, navigator); } return null; @@ -560,55 +554,39 @@ class ShellRoute extends ShellRouteBase { /// implementing a UI with a [BottomNavigationBar], with a persistent navigation /// state for each tab. /// -/// A StatefulShellRoute is created by specifying a List of [StatefulShellBranch] -/// items, each representing a separate stateful branch in the route tree. -/// StatefulShellBranch provides the root routes and the Navigator key ([GlobalKey]) -/// for the branch, as well as an optional initial location. +/// A StatefulShellRoute is created by specifying a List of +/// [StatefulShellBranch] items, each representing a separate stateful branch +/// in the route tree. StatefulShellBranch provides the root routes and the +/// Navigator key ([GlobalKey]) for the branch, as well as an optional initial +/// location. /// /// Like [ShellRoute], either a [builder] or a [pageBuilder] must be provided -/// when creating a StatefulShellRoute. However, these builders differ in that -/// they both accept only a single [StatefulShellBuilder] parameter, used for -/// building the stateful shell for the route. The shell builder in turn accepts -/// a [ShellBodyWidgetBuilder] parameter, used for providing the actual body of -/// the shell. -/// -/// In the ShellBodyWidgetBuilder function, the child parameter -/// ([ShellNavigatorContainer]) is a Widget that contains - and is responsible -/// for managing - the Navigators for the different route branches -/// of this StatefulShellRoute. This widget is meant to be used as the body of -/// the actual shell implementation, for example as the body of [Scaffold] with a -/// [BottomNavigationBar]. -/// -/// The state of a StatefulShellRoute is represented by -/// [StatefulShellRouteState], which can be accessed by calling -/// [StatefulShellRouteState.of]. This state object exposes information such -/// as the current branch index, the state of the route branches etc. The state -/// object also provides support for changing the active branch, i.e. restoring -/// the navigation stack of another branch. This is accomplished using the -/// method [StatefulShellRouteState.goBranch], and providing either a Navigator -/// key, branch name or branch index. For example: +/// when creating a StatefulShellRoute. However, these builders differ slightly +/// in that they accept a [StatefulShellRouteState] parameter instead of a +/// GoRouterState. The StatefulShellRouteState 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 [StatefulShellRouteState.goBranch], for +/// example: /// /// ``` -/// void _onBottomNavigationBarItemTapped(BuildContext context, int index) { -/// final StatefulShellRouteState shellState = StatefulShellRouteState.of(context); +/// void _onItemTapped(StatefulShellRouteState shellState, int index) { /// shellState.goBranch(index: index); /// } /// ``` /// +/// The final child parameter of the builders is a container Widget that manages +/// and maintains the state of the branch Navigators. Typically, a shell is +/// built around this Widget, for example by using it as the body of [Scaffold] +/// with a [BottomNavigationBar]. +/// /// Sometimes greater control is needed over the layout and animations of the /// Widgets representing the branch Navigators. In such cases, a custom -/// implementation can access the Widgets containing the branch Navigators -/// directly through the field [ShellNavigatorContainer.children]. For example: -/// -/// ``` -/// builder: (StatefulShellBuilder shellBuilder) { -/// return shellBuilder.buildShell( -/// (BuildContext context, GoRouterState state, -/// ShellNavigatorContainer child) => -/// TabbedRootScreen(children: child.children), -/// ); -/// } -/// ``` +/// implementation can choose to ignore the child parameter of the builders and +/// instead create a [StatefulShellRouteController], which will manage the state +/// of the StatefulShellRoute. When creating this controller, a builder function +/// is provided to create the container Widget for the branch Navigators. See +/// [ShellNavigatorContainerBuilder] for more details. /// /// Below is a simple example of how a router configuration with /// StatefulShellRoute could be achieved. In this example, a @@ -627,10 +605,9 @@ class ShellRoute extends ShellRouteBase { /// initialLocation: '/a', /// routes: [ /// StatefulShellRoute( -/// builder: (StatefulShellBuilder shellBuilder) { -/// return shellBuilder.buildShell( -/// (BuildContext context, GoRouterState state, Widget child) => -/// ScaffoldWithNavBar(body: child)); +/// builder: (BuildContext context, StatefulShellRouteState state, +/// Widget child) { +/// return ScaffoldWithNavBar(shellState: state, body: child); /// }, /// branches: [ /// /// The first branch, i.e. tab 'A' @@ -677,14 +654,6 @@ class ShellRoute extends ShellRouteBase { /// ); /// ``` /// -/// To access the current state of this route, to for instance access the -/// index of the current route branch - use the method -/// [StatefulShellRouteState.of]. For example: -/// -/// ``` -/// final StatefulShellRouteState shellState = StatefulShellRouteState.of(context); -/// ``` -/// /// See [Stateful Nested Navigation](https://github.com/flutter/packages/blob/main/packages/go_router/example/lib/stateful_shell_route.dart) /// for a complete runnable example using StatefulShellRoute. class StatefulShellRoute extends ShellRouteBase { @@ -710,41 +679,36 @@ class StatefulShellRoute extends ShellRouteBase { /// The widget builder for a stateful shell route. /// - /// Similar to [GoRoute.builder], but this builder function accepts a single - /// [StatefulShellBuilder] parameter, used for building the stateful shell for - /// this route. The shell builder in turn accepts a [ShellBodyWidgetBuilder] - /// parameter, used for providing the actual body of the shell. + /// 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. /// - /// Example: - /// ``` - /// StatefulShellRoute( - /// builder: (StatefulShellBuilder shellBuilder) { - /// return shellBuilder.buildShell( - /// (BuildContext context, GoRouterState state, Widget child) => - /// ScaffoldWithNavBar(body: child)); - /// }, - /// ) - /// ``` + /// Instead of a GoRouterState, this builder function accepts a + /// [StatefulShellRouteState] object, which can be used to access information + /// about which branch is active, and also to navigate to a different branch + /// (using [StatefulShellRouteState.goBranch]). + /// + /// Custom implementations may choose to ignore the child parameter passed to + /// the builder function, and instead use [StatefulShellRouteController] to + /// create a custom container for the branch Navigators. final StatefulShellRouteBuilder? builder; /// The page builder for a stateful shell route. /// - /// Similar to [GoRoute.pageBuilder], This builder function accepts a single - /// [StatefulShellBuilder] parameter, used for building the stateful shell for - /// this route. The shell builder in turn accepts a [ShellBodyWidgetBuilder] - /// parameter, used for providing the actual body of the shell. + /// 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. + /// + /// Instead of a GoRouterState, this builder function accepts a + /// [StatefulShellRouteState] object, which can be used to access information + /// about which branch is active, and also to navigate to a different branch + /// (using [StatefulShellRouteState.goBranch]). /// - /// Example: - /// ``` - /// StatefulShellRoute( - /// pageBuilder: (StatefulShellBuilder shellBuilder) { - /// final Widget statefulShell = shellBuilder.buildShell( - /// (BuildContext context, GoRouterState state, Widget child) => - /// ScaffoldWithNavBar(body: child)); - /// return MaterialPage(child: statefulShell); - /// }, - /// ) - /// ``` + /// Custom implementations may choose to ignore the child parameter passed to + /// the builder function, and instead use [StatefulShellRouteController] to + /// create a custom container for the branch Navigators. final StatefulShellRoutePageBuilder? pageBuilder; /// Representations of the different stateful route branches that this @@ -758,7 +722,9 @@ class StatefulShellRoute extends ShellRouteBase { Widget? buildWidget(BuildContext context, GoRouterState state, ShellNavigatorBuilder navigatorBuilder) { if (builder != null) { - return builder!(StatefulShellBuilder(this, navigatorBuilder)); + final _StatefulNavigationShell shell = + _createShell(context, state, navigatorBuilder); + return builder!(context, shell.shellRouteState, shell); } return null; } @@ -767,7 +733,9 @@ class StatefulShellRoute extends ShellRouteBase { Page? buildPage(BuildContext context, GoRouterState state, ShellNavigatorBuilder navigatorBuilder) { if (pageBuilder != null) { - return pageBuilder!(StatefulShellBuilder(this, navigatorBuilder)); + final _StatefulNavigationShell shell = + _createShell(context, state, navigatorBuilder); + return pageBuilder!(context, shell.shellRouteState, shell); } return null; } @@ -780,6 +748,13 @@ class StatefulShellRoute extends ShellRouteBase { return branch!.navigatorKey; } + _StatefulNavigationShell _createShell(BuildContext context, + GoRouterState state, ShellNavigatorBuilder navigatorBuilder) { + final _StatefulShellRouteState shellRouteState = + _StatefulShellRouteState._(this, state, navigatorBuilder); + return _StatefulNavigationShell._(shellRouteState); + } + static List _routes(List branches) => branches.expand((StatefulShellBranch e) => e.routes).toList(); @@ -802,33 +777,6 @@ class StatefulShellRoute extends ShellRouteBase { } } -/// Builds the Widget managing a StatefulShellRoute. -class StatefulShellBuilder { - /// Constructs a [StatefulShellBuilder]. - StatefulShellBuilder(this._shellRoute, this._builder); - - final StatefulShellRoute _shellRoute; - final ShellNavigatorBuilder _builder; - - /// Builds the Widget managing a StatefulShellRoute. - Widget buildShell(ShellBodyWidgetBuilder body) { - return _StatefulNavigationShell._( - shellRoute: _shellRoute, - navigatorBuilder: _builder, - shellBodyWidgetBuilder: body, - ); - } -} - -/// Widget containing the Navigators for the branches in a [StatefulShellRoute]. -abstract class ShellNavigatorContainer extends StatelessWidget { - /// Constructs a [ShellNavigatorContainer]. - const ShellNavigatorContainer({super.key}); - - /// The children (i.e. Navigators) of this ShellNavigatorContainer. - List get children; -} - /// Representation of a separate branch in a stateful navigation tree, used to /// configure [StatefulShellRoute]. /// @@ -836,9 +784,14 @@ abstract class ShellNavigatorContainer extends StatelessWidget { /// 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 [StatefulShellBranchState]). A [navigatorKey] -/// can be useful to provide in case it's necessary to access the [Navigator] -/// created for this branch elsewhere. +/// using the goBranch method in [StatefulShellRouteState]). +/// +/// A separate [Navigator] will be built for each StatefulShellBranch in a +/// [StatefulShellRoute], and the routes of this branch will be placed onto that +/// Navigator instead of the root Navigator. A custom [navigatorKey] can be +/// provided when creating a StatefulShellBranch, which can be useful when the +/// Navigator needs to be accessed elsewhere. If no key is provided, a default +/// one will be created. @immutable class StatefulShellBranch { /// Constructs a [StatefulShellBranch]. @@ -868,7 +821,7 @@ class StatefulShellBranch { /// be used (i.e. first element in [routes], or a descendant). The default /// location is used when loading the branch for the first time (for instance /// when switching branch using the goBranch method in - /// [StatefulShellBranchState]). + /// [StatefulShellRouteState]). final String? initialLocation; /// Whether this route branch should be preloaded when the associated @@ -892,37 +845,73 @@ class StatefulShellBranch { final List? observers; } -/// [StatefulShellRouteState] extension, providing support for fetching the state -/// associated with the nearest [StatefulShellRoute] in the Widget tree. -extension StatefulShellRouteStateContext on StatefulShellRouteState { - /// Gets the state for the nearest stateful shell route in the Widget tree. - static StatefulShellRouteState of(BuildContext context) { - final _InheritedStatefulNavigationShell? inherited = - context.dependOnInheritedWidgetOfExactType< - _InheritedStatefulNavigationShell>(); - assert(inherited != null, - 'No InheritedStatefulNavigationShell found in context'); - return inherited!.routeState; - } -} +/// The snapshot of the current state of a [StatefulShellRoute]. +/// +/// Note that this an immutable class, that represents the snapshot of the state +/// of a StatefulShellRoute at a given point in time. Therefore, instances of +/// this object should not be cached, but instead passed down from the builder +/// functions of StatefulShellRoute. +@immutable +abstract class StatefulShellRouteState { + /// The associated [StatefulShellRoute] + StatefulShellRoute get route; + + /// The GoRouterState associate with [route]. + GoRouterState get routerState; -/// [InheritedWidget] for providing a reference to the closest -/// [_StatefulNavigationShellState]. -class _InheritedStatefulNavigationShell extends InheritedWidget { - /// Constructs an [_InheritedStatefulNavigationShell]. - const _InheritedStatefulNavigationShell({ - required super.child, - required this.routeState, + /// The index of the currently active [StatefulShellBranch]. + /// + /// Corresponds to the index of the branch in the List returned from + /// branchBuilder of [StatefulShellRoute]. + int get currentIndex; + + /// The Navigator key of the current navigator. + GlobalKey get currentNavigatorKey; + + /// Navigate to the current location of the shell navigator with the provided + /// index. + /// + /// This method will switch the currently active [Navigator] for the + /// [StatefulShellRoute] by replacing the current navigation stack with the + /// one of the route branch identified by the provided index. If resetLocation + /// is true, the branch will be reset to its initial location + /// (see [StatefulShellBranch.initialLocation]). + void goBranch({ + required int index, }); +} - /// The [StatefulShellRouteState] that is exposed by this InheritedWidget. - final StatefulShellRouteState routeState; +/// Builder for a custom container for shell route Navigators. +typedef ShellNavigatorContainerBuilder = Widget Function(BuildContext context, + StatefulShellRouteState shellRouteState, List children); - @override - bool updateShouldNotify( - covariant _InheritedStatefulNavigationShell oldWidget) { - return routeState != oldWidget.routeState; - } +/// 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, StatefulShellRouteController can be used in +/// the builder or pageBuilder methods of StatefulShellRoute to facilitate this. +/// The container is created using the provided [ShellNavigatorContainerBuilder], +/// where the List of Widgets represent the Navigators for each branch. +/// +/// Example: +/// ``` +/// builder: (BuildContext context, StatefulShellRouteState state, Widget child) { +/// return StatefulShellRouteController( +/// shellRouteState: state, +/// containerBuilder: (_, __, List children) => MyCustomShell(shellState: state, children: children), +/// ); +/// } +/// ``` +abstract class StatefulShellRouteController extends StatefulWidget { + /// Constructs a [StatefulShellRouteController]. + factory StatefulShellRouteController( + {required StatefulShellRouteState shellRouteState, + required ShellNavigatorContainerBuilder containerBuilder}) => + _StatefulNavigationShell._(shellRouteState as _StatefulShellRouteState, + containerBuilder: containerBuilder); + + const StatefulShellRouteController._(); } /// Widget that manages and maintains the state of a [StatefulShellRoute], @@ -940,22 +929,22 @@ class _InheritedStatefulNavigationShell extends InheritedWidget { /// However, implementors can choose to disregard this and use an alternate /// container around the branch navigators /// (see [StatefulShellRouteState.children]) instead. -class _StatefulNavigationShell extends StatefulWidget { +class _StatefulNavigationShell extends StatefulShellRouteController { /// Constructs an [_StatefulNavigationShell]. - const _StatefulNavigationShell._({ - required this.shellRoute, - required this.navigatorBuilder, - required this.shellBodyWidgetBuilder, - }); - - /// The associated [StatefulShellRoute] - final StatefulShellRoute shellRoute; + const _StatefulNavigationShell._(this.shellRouteState, + {ShellNavigatorContainerBuilder? containerBuilder}) + : _containerBuilder = containerBuilder ?? _defaultChildBuilder, + super._(); + + static Widget _defaultChildBuilder(BuildContext context, + StatefulShellRouteState shellRouteState, List children) { + return _IndexedStackedRouteBranchContainer( + currentIndex: shellRouteState.currentIndex, children: children); + } - /// The shell navigator builder. - final ShellNavigatorBuilder navigatorBuilder; + final _StatefulShellRouteState shellRouteState; - /// The shell body widget builder. - final ShellBodyWidgetBuilder shellBodyWidgetBuilder; + final ShellNavigatorContainerBuilder _containerBuilder; @override State createState() => _StatefulNavigationShellState(); @@ -965,13 +954,12 @@ class _StatefulNavigationShell extends StatefulWidget { class _StatefulNavigationShellState extends State<_StatefulNavigationShell> { final Map _navigatorCache = {}; - late _StatefulShellRouteState _routeState; + final List<_StatefulShellBranchState> _branchStates = + <_StatefulShellBranchState>[]; - List get _branches => widget.shellRoute.branches; + _StatefulShellRouteState get shellState => widget.shellRouteState; - GoRouterState get _currentGoRouterState => widget.navigatorBuilder.state; - GlobalKey get _currentNavigatorKey => - widget.navigatorBuilder.navigatorKeyForCurrentRoute; + StatefulShellRoute get _route => widget.shellRouteState.route; Widget? _navigatorForBranch(StatefulShellBranch branch) { return _navigatorCache[branch.navigatorKey]; @@ -983,19 +971,17 @@ class _StatefulNavigationShellState extends State<_StatefulNavigationShell> { : _navigatorCache.remove(branch.navigatorKey); } - int _findCurrentIndex() { - final int index = _branches.indexWhere( - (StatefulShellBranch e) => e.navigatorKey == _currentNavigatorKey); - assert(index >= 0); - return index; - } - - void _switchActiveBranch(StatefulShellBranchState branchState) { + void _switchBranch(int index) { final GoRouter goRouter = GoRouter.of(context); - final GoRouterState? routeState = branchState.routeState; - if (routeState != null) { - goRouter.goState(routeState, context).onError( - (_, __) => goRouter.go(_defaultBranchLocation(branchState.branch))); + final _StatefulShellBranchState branchState = _branchStates[index]; + final RouteMatchList? matchList = + branchState.matchList?.modifiableMatchList; + if (matchList != null) { + goRouter.routeInformationParser + .processRedirection(matchList, context) + .then(goRouter.routerDelegate.setNewRoutePath) + .onError((_, __) => + goRouter.go(_defaultBranchLocation(branchState.branch))); } else { goRouter.go(_defaultBranchLocation(branchState.branch)); } @@ -1009,13 +995,13 @@ class _StatefulNavigationShellState extends State<_StatefulNavigationShell> { } void _preloadBranches() { - final List<_StatefulShellBranchState> states = _routeState._branchStates; - for (_StatefulShellBranchState state in states) { - if (state.branch.preload && !state.isLoaded) { - state = _updateStatefulShellBranchState(state, loaded: true); - _preloadBranch(state).then((_StatefulShellBranchState branchState) { + for (int i = 0; i < _branchStates.length; i++) { + if (_branchStates[i].branch.preload && !_branchStates[i].isLoaded) { + _branchStates[i] = _updateBranchState(_branchStates[i], loaded: true); + _preloadBranch(_branchStates[i]) + .then((_StatefulShellBranchState branchState) { setState(() { - _updateRouteBranchState(branchState); + _updateBranchStateList(branchState); }); }); } @@ -1024,99 +1010,97 @@ class _StatefulNavigationShellState extends State<_StatefulNavigationShell> { Future<_StatefulShellBranchState> _preloadBranch( _StatefulShellBranchState branchState) { - final Future navigatorBuilder = - widget.navigatorBuilder.buildPreloadedShellNavigator( - context: context, - location: _defaultBranchLocation(branchState.branch), - parentShellRoute: widget.shellRoute, - navigatorKey: branchState.navigatorKey, - observers: branchState.branch.observers, - restorationScopeId: branchState.branch.restorationScopeId, - ); + final Future navigator = _preloadBranchNavigator(branchState); - return navigatorBuilder.then((Widget? navigator) { - return _updateStatefulShellBranchState( + return navigator.then((Widget? navigator) { + return _updateBranchState( branchState, navigator: navigator, ); }); } - void _updateRouteBranchState(_StatefulShellBranchState branchState, - {int? currentIndex}) { - final List<_StatefulShellBranchState> existingStates = - _routeState._branchStates; - final List<_StatefulShellBranchState> newStates = - <_StatefulShellBranchState>[]; + Future _preloadBranchNavigator( + _StatefulShellBranchState branchState) { + final GlobalKey navigatorKey = branchState.navigatorKey; + final String location = _defaultBranchLocation(branchState.branch); + + final RouteBuilder routeBuilder = shellState.navigatorBuilder.routeBuilder; + + // Parse a RouteMatchList from location and handle any redirects + final GoRouteInformationParser parser = + GoRouter.of(context).routeInformationParser; + final Future routeMatchList = + parser.parseRouteInformationWithDependencies( + RouteInformation(location: _defaultBranchLocation(branchState.branch)), + context, + ); - // Build a new list of the current StatefulShellBranchStates, with an - // updated state for the current branch etc. - for (final StatefulShellBranch branch in _branches) { - if (branch.navigatorKey == branchState.navigatorKey) { - newStates.add(branchState); + Widget? buildNavigator(RouteMatchList matchList) { + // Find the index of fromRoute in the match list + final int parentShellRouteIndex = + matchList.matches.indexWhere((RouteMatch e) => e.route == _route); + if (parentShellRouteIndex >= 0) { + final int startIndex = parentShellRouteIndex + 1; + final GlobalKey routeNavigatorKey = + _route.navigatorKeyForSubRoute(matchList.matches[startIndex].route); + assert( + navigatorKey == routeNavigatorKey, + 'Incorrect shell navigator key ' + 'for preloaded match list for location "$location"'); + + return routeBuilder.buildPreloadedNestedNavigator( + context, + matchList, + parentShellRouteIndex + 1, + true, + navigatorKey, + restorationScopeId: branchState.branch.restorationScopeId, + observers: branchState.branch.observers, + ); } else { - newStates.add(existingStates.firstWhereOrNull( - (StatefulShellBranchState e) => e.branch == branch) ?? - _createStatefulShellBranchState(branch)); + return null; } } - // Remove any obsolete cached Navigators - final Set validKeys = - _branches.map((StatefulShellBranch e) => e.navigatorKey).toSet(); - _navigatorCache.removeWhere((Key key, _) => !validKeys.contains(key)); - - _routeState = _routeState._copy( - branchStates: newStates, - currentIndex: currentIndex, - ); + return routeMatchList.then(buildNavigator); } - void _updateRouteStateFromWidget() { - final int index = _findCurrentIndex(); - final StatefulShellBranch branch = _branches[index]; + void _updateCurrentBranchStateFromWidget() { + // Connect the goBranch function in the current StatefulShellRouteState + // to the _switchBranch implementation in this class. + shellState._switchBranch.complete(_switchBranch); - final Widget currentNavigator = - widget.navigatorBuilder.buildNavigatorForCurrentRoute( + final StatefulShellBranch branch = _route.branches[shellState.currentIndex]; + final ShellNavigatorBuilder navigatorBuilder = shellState.navigatorBuilder; + final Widget currentNavigator = navigatorBuilder.buildNavigator( observers: branch.observers, restorationScopeId: branch.restorationScopeId, ); // Update or create a new StatefulShellBranchState for the current branch // (i.e. the arguments currently provided to the Widget). - _StatefulShellBranchState? currentBranchState = _routeState._branchStates - .firstWhereOrNull((_StatefulShellBranchState e) => e.branch == branch); - if (currentBranchState != null) { - currentBranchState = _updateStatefulShellBranchState( - currentBranchState, - navigator: currentNavigator, - routeState: _currentGoRouterState, - ); - } else { - currentBranchState = _createStatefulShellBranchState( - branch, - navigator: currentNavigator, - routeState: _currentGoRouterState, - ); - } - - _updateRouteBranchState( + _StatefulShellBranchState currentBranchState = _branchStates.firstWhere( + (_StatefulShellBranchState e) => e.branch == branch, + orElse: () => _StatefulShellBranchState._(branch)); + currentBranchState = _updateBranchState( currentBranchState, - currentIndex: index, + navigator: currentNavigator, + matchList: navigatorBuilder.routeMatchList.unmodifiableMatchList(), ); - _preloadBranches(); + _updateBranchStateList(currentBranchState); } - _StatefulShellBranchState _updateStatefulShellBranchState( + _StatefulShellBranchState _updateBranchState( _StatefulShellBranchState branchState, { Widget? navigator, - GoRouterState? routeState, + UnmodifiableRouteMatchList? matchList, bool? loaded, }) { bool dirty = false; - if (routeState != null) { - dirty = branchState.routeState != routeState; + if (matchList != null) { + dirty = branchState.matchList != matchList; } if (navigator != null) { @@ -1137,80 +1121,62 @@ class _StatefulNavigationShellState extends State<_StatefulNavigationShell> { if (dirty) { return branchState._copy( isLoaded: isLoaded, - routeState: routeState, + matchList: matchList, ); } else { return branchState; } } - _StatefulShellBranchState _createStatefulShellBranchState( - StatefulShellBranch branch, { - Widget? navigator, - GoRouterState? routeState, - }) { - if (navigator != null) { - _setNavigatorForBranch(branch, navigator); - } - return _StatefulShellBranchState._( - branch: branch, - routeState: routeState, - ); - } + void _updateBranchStateList(_StatefulShellBranchState currentBranchState) { + final List<_StatefulShellBranchState> existingStates = + List<_StatefulShellBranchState>.from(_branchStates); + _branchStates.clear(); - void _setupInitialStatefulShellRouteState() { - final List<_StatefulShellBranchState> states = _branches - .map((StatefulShellBranch e) => _createStatefulShellBranchState(e)) - .toList(); + // Build a new list of the current StatefulShellBranchStates, with an + // updated state for the current branch etc. + for (final StatefulShellBranch branch in _route.branches) { + if (branch.navigatorKey == currentBranchState.navigatorKey) { + _branchStates.add(currentBranchState); + } else { + _branchStates.add(existingStates.firstWhereOrNull( + (_StatefulShellBranchState e) => e.branch == branch) ?? + _StatefulShellBranchState._(branch)); + } + } - _routeState = _StatefulShellRouteState._( - widget.shellRoute, - states, - 0, - _switchActiveBranch, - ); + // Remove any obsolete cached Navigators + final Set validKeys = + _route.branches.map((StatefulShellBranch e) => e.navigatorKey).toSet(); + _navigatorCache.removeWhere((Key key, _) => !validKeys.contains(key)); } @override void initState() { super.initState(); - _setupInitialStatefulShellRouteState(); + _updateCurrentBranchStateFromWidget(); } @override void didUpdateWidget(covariant _StatefulNavigationShell oldWidget) { super.didUpdateWidget(oldWidget); - _updateRouteStateFromWidget(); + _updateCurrentBranchStateFromWidget(); + _preloadBranches(); } @override void didChangeDependencies() { super.didChangeDependencies(); - _updateRouteStateFromWidget(); + _preloadBranches(); } @override Widget build(BuildContext context) { - final List children = _branches + final List children = _route.branches .map((StatefulShellBranch branch) => _BranchNavigatorProxy( branch: branch, navigatorForBranch: _navigatorForBranch)) .toList(); - - return _InheritedStatefulNavigationShell( - routeState: _routeState, - child: Builder(builder: (BuildContext context) { - // This Builder Widget is mainly used to make it possible to access the - // StatefulShellRouteState via the BuildContext in the ShellRouteBuilder - final ShellBodyWidgetBuilder shellWidgetBuilder = - widget.shellBodyWidgetBuilder; - return shellWidgetBuilder( - context, - _currentGoRouterState, - _IndexedStackedRouteBranchContainer( - routeState: _routeState, children: children), - ); - }), - ); + return widget._containerBuilder(context, shellState, children); } } @@ -1235,18 +1201,16 @@ class _BranchNavigatorProxy extends StatelessWidget { /// Default implementation of a container widget for the [Navigator]s of the /// route branches. This implementation uses an [IndexedStack] as a container. -class _IndexedStackedRouteBranchContainer extends ShellNavigatorContainer { +class _IndexedStackedRouteBranchContainer extends StatelessWidget { const _IndexedStackedRouteBranchContainer( - {required this.routeState, required this.children}); + {required this.currentIndex, required this.children}); - final StatefulShellRouteState routeState; + final int currentIndex; - @override final List children; @override Widget build(BuildContext context) { - final int currentIndex = routeState.currentIndex; final List stackItems = children .mapIndexed((int index, Widget child) => _buildRouteBranchContainer(context, currentIndex == index, child)) @@ -1267,54 +1231,37 @@ class _IndexedStackedRouteBranchContainer extends ShellNavigatorContainer { } } -extension _StatefulShellBranchStateHelper on StatefulShellBranchState { - GlobalKey get navigatorKey => branch.navigatorKey; -} - -typedef _BranchSwitcher = void Function(StatefulShellBranchState); +typedef _BranchSwitcher = void Function(int); -/// The snapshot of the current state of a [StatefulShellRoute]. -/// -/// Note that this an immutable class, that represents the snapshot of the state -/// of a StatefulShellRoute at a given point in time. Therefore, instances of -/// this object should not be stored, but instead fetched fresh when needed, -/// using the method [StatefulShellRouteState.of]. -@immutable +/// Internal [StatefulShellRouteState] implementation. class _StatefulShellRouteState implements StatefulShellRouteState { - /// Constructs a [StatefulShellRouteState]. - const _StatefulShellRouteState._( + /// Constructs a [_StatefulShellRouteState]. + _StatefulShellRouteState._( this.route, - this._branchStates, - this.currentIndex, - _BranchSwitcher switchActiveBranch, - ) : _switchActiveBranch = switchActiveBranch; - - /// Constructs a copy of this [StatefulShellRouteState], with updated values - /// for some of the fields. - _StatefulShellRouteState _copy( - {List<_StatefulShellBranchState>? branchStates, int? currentIndex}) { - return _StatefulShellRouteState._( - route, - branchStates ?? _branchStates, - currentIndex ?? this.currentIndex, - _switchActiveBranch, - ); + this.routerState, + this.navigatorBuilder, + ) : currentIndex = + _indexOfBranchNavigatorKey(route, navigatorBuilder.navigatorKey); + + static int _indexOfBranchNavigatorKey( + StatefulShellRoute route, GlobalKey navigatorKey) { + final int index = route.branches.indexWhere( + (StatefulShellBranch branch) => branch.navigatorKey == navigatorKey); + assert(index >= 0); + return index; } /// The associated [StatefulShellRoute] @override final StatefulShellRoute route; - final List<_StatefulShellBranchState> _branchStates; - - /// The state for all separate route branches associated with a - /// [StatefulShellRoute]. + /// The current route state associated with the [StatefulShellRoute]. @override - List get branchStates => _branchStates; + final GoRouterState routerState; - /// The state associated with the current [StatefulShellBranch]. - @override - StatefulShellBranchState get currentBranchState => branchStates[currentIndex]; + /// The ShellNavigatorBuilder responsible for building the Navigator for the + /// current [StatefulShellBranch] + final ShellNavigatorBuilder navigatorBuilder; /// The index of the currently active [StatefulShellBranch]. /// @@ -1326,9 +1273,11 @@ class _StatefulShellRouteState implements StatefulShellRouteState { /// The Navigator key of the current navigator. @override GlobalKey get currentNavigatorKey => - currentBranchState.branch.navigatorKey; + route.branches[currentIndex].navigatorKey; - final _BranchSwitcher _switchActiveBranch; + /// Completer for a branch switcher function, that will be populated by + /// _StatefulNavigationShellState. + final Completer<_BranchSwitcher> _switchBranch = Completer<_BranchSwitcher>(); /// Navigate to the current location of the shell navigator with the provided /// index. @@ -1342,77 +1291,60 @@ class _StatefulShellRouteState implements StatefulShellRouteState { void goBranch({ required int index, }) { - _switchActiveBranch(branchStates[index]); - } - - @override - bool operator ==(Object other) { - if (identical(other, this)) { - return true; - } - if (other is! _StatefulShellRouteState) { - return false; - } - return other.route == route && - listEquals(other._branchStates, _branchStates) && - other.currentIndex == currentIndex; + assert(index >= 0 && index < route.branches.length); + _switchBranch.future + .then((_BranchSwitcher switchBranch) => switchBranch(index)); } - - @override - int get hashCode => Object.hash(route, currentIndex, currentIndex); } /// The snapshot of the current state for a particular route branch /// ([StatefulShellBranch]) in a [StatefulShellRoute]. /// /// Note that this an immutable class, that represents the snapshot of the state -/// of a StatefulShellBranchState at a given point in time. Therefore, instances of -/// this object should not be stored, but instead fetched fresh when needed, -/// via the [StatefulShellRouteState] returned by the method -/// [StatefulShellRouteState.of]. +/// of a StatefulShellBranchState at a given point in time. @immutable -class _StatefulShellBranchState implements StatefulShellBranchState { +class _StatefulShellBranchState { /// Constructs a [StatefulShellBranchState]. - const _StatefulShellBranchState._({ - required this.branch, + const _StatefulShellBranchState._( + this.branch, { this.isLoaded = false, - this.routeState, + this.matchList, }); /// Constructs a copy of this [StatefulShellBranchState], with updated values for /// some of the fields. - _StatefulShellBranchState _copy({bool? isLoaded, GoRouterState? routeState}) { + _StatefulShellBranchState _copy( + {bool? isLoaded, UnmodifiableRouteMatchList? matchList}) { return _StatefulShellBranchState._( - branch: branch, + branch, isLoaded: isLoaded ?? this.isLoaded, - routeState: routeState ?? this.routeState, + matchList: matchList ?? this.matchList, ); } /// The associated [StatefulShellBranch] - @override final StatefulShellBranch branch; - /// The current GoRouterState associated with the branch. - @override - final GoRouterState? routeState; + /// The current navigation stack for the branch. + final UnmodifiableRouteMatchList? matchList; /// Returns true if this branch has been loaded (i.e. visited once or /// pre-loaded). - @override final bool isLoaded; + GlobalKey get navigatorKey => branch.navigatorKey; + @override bool operator ==(Object other) { if (identical(other, this)) { return true; } - if (other is! StatefulShellBranchState) { + if (other is! _StatefulShellBranchState) { return false; } - return other.branch == branch && other.routeState == routeState; + return other.branch == branch && other.matchList == matchList; } @override - int get hashCode => Object.hash(branch, routeState); + int get hashCode => Object.hash(branch, matchList); } diff --git a/packages/go_router/lib/src/router.dart b/packages/go_router/lib/src/router.dart index aed73ab2aec6..8727434755ae 100644 --- a/packages/go_router/lib/src/router.dart +++ b/packages/go_router/lib/src/router.dart @@ -200,14 +200,6 @@ class GoRouter extends ChangeNotifier implements RouterConfig { extra: extra, ); - /// Restore the location represented by the provided state. - Future goState(GoRouterState state, BuildContext context) { - final RouteMatchList matchList = state.routeMatchList.modifiableMatchList; - return routeInformationParser - .processRedirection(matchList, context) - .then(routerDelegate.setNewRoutePath); - } - /// Push a URI location onto the page stack w/ optional query parameters, e.g. /// `/family/f2/person/p1?color=blue` void push(String location, {Object? extra}) { diff --git a/packages/go_router/lib/src/shell_state.dart b/packages/go_router/lib/src/shell_state.dart deleted file mode 100644 index 83f68849525d..000000000000 --- a/packages/go_router/lib/src/shell_state.dart +++ /dev/null @@ -1,74 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'package:flutter/widgets.dart'; - -import 'route.dart'; -import 'state.dart'; - -/// The snapshot of the current state of a [StatefulShellRoute]. -/// -/// Note that this an immutable class, that represents the snapshot of the state -/// of a StatefulShellRoute at a given point in time. Therefore, instances of -/// this object should not be stored, but instead fetched fresh when needed, -/// using the method [StatefulShellRouteState.of]. -@immutable -abstract class StatefulShellRouteState { - /// The associated [StatefulShellRoute] - StatefulShellRoute get route; - - /// The state for all separate route branches associated with a - /// [StatefulShellRoute]. - List get branchStates; - - /// The state associated with the current [StatefulShellBranch]. - StatefulShellBranchState get currentBranchState; - - /// The index of the currently active [StatefulShellBranch]. - /// - /// Corresponds to the index of the branch in the List returned from - /// branchBuilder of [StatefulShellRoute]. - int get currentIndex; - - /// The Navigator key of the current navigator. - GlobalKey get currentNavigatorKey; - - /// Navigate to the current location of the shell navigator with the provided - /// index. - /// - /// This method will switch the currently active [Navigator] for the - /// [StatefulShellRoute] by replacing the current navigation stack with the - /// one of the route branch identified by the provided index. If resetLocation - /// is true, the branch will be reset to its initial location - /// (see [StatefulShellBranch.initialLocation]). - void goBranch({ - required int index, - }); - - /// Gets the state for the nearest stateful shell route in the Widget tree. - static StatefulShellRouteState of(BuildContext context) { - return StatefulShellRouteStateContext.of(context); - } -} - -/// The snapshot of the current state for a particular route branch -/// ([StatefulShellBranch]) in a [StatefulShellRoute]. -/// -/// Note that this an immutable class, that represents the snapshot of the state -/// of a StatefulShellBranchState at a given point in time. Therefore, instances of -/// this object should not be stored, but instead fetched fresh when needed, -/// via the [StatefulShellRouteState] returned by the method -/// [StatefulShellRouteState.of]. -@immutable -abstract class StatefulShellBranchState { - /// The associated [StatefulShellBranch] - StatefulShellBranch get branch; - - /// The current GoRouterState associated with the branch. - GoRouterState? get routeState; - - /// Returns true if this branch has been loaded (i.e. visited once or - /// pre-loaded). - bool get isLoaded; -} diff --git a/packages/go_router/lib/src/state.dart b/packages/go_router/lib/src/state.dart index e8322dbe2a74..fd8b904cafbe 100644 --- a/packages/go_router/lib/src/state.dart +++ b/packages/go_router/lib/src/state.dart @@ -2,12 +2,10 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'package:flutter/foundation.dart'; import 'package:flutter/widgets.dart'; import '../go_router.dart'; import 'configuration.dart'; -import 'matching.dart'; import 'misc/errors.dart'; /// The route state during routing. @@ -17,8 +15,7 @@ import 'misc/errors.dart'; class GoRouterState { /// Default constructor for creating route state during routing. const GoRouterState( - this._configuration, - this._routeMatchList, { + this._configuration, { required this.location, required this.subloc, required this.name, @@ -36,13 +33,6 @@ class GoRouterState { // See https://github.com/flutter/flutter/issues/107729 final RouteConfiguration _configuration; - /// Snapshot of the current route match list. - /// - /// Use to restore the navigation stack based on a GoRouterState, and also - /// to make two GoRouterState instances from different match lists unique - /// (i.e. not equal). - final UnmodifiableRouteMatchList _routeMatchList; - /// The full location of the route, e.g. /family/f2/person/p1 final String location; @@ -150,29 +140,17 @@ class GoRouterState { other.name == name && other.path == path && other.fullpath == fullpath && - mapEquals(other.params, params) && - mapEquals(other.queryParams, queryParams) && - mapEquals(other.queryParametersAll, queryParametersAll) && + other.params == params && + other.queryParams == queryParams && + other.queryParametersAll == queryParametersAll && other.extra == extra && other.error == error && - other.pageKey == pageKey && - other._routeMatchList == _routeMatchList; + other.pageKey == pageKey; } @override - int get hashCode => Object.hash( - location, - subloc, - name, - path, - fullpath, - params, - queryParams, - queryParametersAll, - extra, - error, - pageKey, - _routeMatchList); + int get hashCode => Object.hash(location, subloc, name, path, fullpath, + params, queryParams, queryParametersAll, extra, error, pageKey); } /// An inherited widget to host a [GoRouterStateRegistry] for the subtree. @@ -273,11 +251,3 @@ class GoRouterStateRegistry extends ChangeNotifier { } } } - -/// Internal extension to expose the routeMatchList associated with a [GoRouterState]. -extension GoRouterStateInternal on GoRouterState { - /// The route match list associated with this [GoRouterState]. - UnmodifiableRouteMatchList get routeMatchList { - return _routeMatchList; - } -} diff --git a/packages/go_router/lib/src/typedefs.dart b/packages/go_router/lib/src/typedefs.dart index 2705a6dc92fe..5ba23236af71 100644 --- a/packages/go_router/lib/src/typedefs.dart +++ b/packages/go_router/lib/src/typedefs.dart @@ -36,22 +36,15 @@ typedef ShellRoutePageBuilder = Page Function( /// The widget builder for [StatefulShellRoute]. typedef StatefulShellRouteBuilder = Widget Function( - StatefulShellBuilder navigatorBuilder, + BuildContext context, + StatefulShellRouteState state, + Widget child, ); /// The page builder for [StatefulShellRoute]. typedef StatefulShellRoutePageBuilder = Page Function( - StatefulShellBuilder navigatorBuilder, -); - -/// The shell body widget builder for [StatefulShellRoute]. -typedef ShellBodyWidgetBuilder = Widget Function( - BuildContext context, GoRouterState state, ShellNavigatorContainer child); - -/// The signature of the navigatorBuilder callback. -typedef GoRouterNavigatorBuilder = Widget Function( BuildContext context, - GoRouterState state, + StatefulShellRouteState state, Widget child, ); diff --git a/packages/go_router/test/builder_test.dart b/packages/go_router/test/builder_test.dart index 25eb319de7a5..2231b842897b 100644 --- a/packages/go_router/test/builder_test.dart +++ b/packages/go_router/test/builder_test.dart @@ -435,11 +435,9 @@ void main() { navigatorKey: rootNavigatorKey, routes: [ StatefulShellRoute( - builder: (StatefulShellBuilder shellBuilder) { - return shellBuilder.buildShell( - (BuildContext context, GoRouterState state, Widget child) => - _HomeScreen(child: child)); - }, + builder: (BuildContext context, StatefulShellRouteState state, + Widget child) => + _HomeScreen(child: child), branches: [ StatefulShellBranch( navigatorKey: shellNavigatorKey, @@ -536,13 +534,14 @@ class _BuilderTestWidget extends StatelessWidget { }, restorationScopeId: null, observers: [], + onPopPage: (_, __, ___) => false, ); } @override Widget build(BuildContext context) { return MaterialApp( - home: builder.tryBuild(context, matches, (_, __, ___) => false, false, + home: builder.tryBuild(context, matches, false, routeConfiguration.navigatorKey, , GoRouterState>{}), // builder: (context, child) => , ); diff --git a/packages/go_router/test/go_router_test.dart b/packages/go_router/test/go_router_test.dart index 0a527c918619..0eb9fcd9a5da 100644 --- a/packages/go_router/test/go_router_test.dart +++ b/packages/go_router/test/go_router_test.dart @@ -2407,12 +2407,10 @@ void main() { StatefulShellRouteState? routeState; final List routes = [ StatefulShellRoute( - builder: (StatefulShellBuilder shellBuilder) { - return shellBuilder.buildShell( - (BuildContext context, GoRouterState state, Widget child) { - routeState = StatefulShellRouteState.of(context); - return child; - }); + builder: (BuildContext context, StatefulShellRouteState state, + Widget child) { + routeState = state; + return child; }, branches: [ StatefulShellBranch( @@ -2942,12 +2940,10 @@ void main() { final List routes = [ StatefulShellRoute( - builder: (StatefulShellBuilder shellBuilder) { - return shellBuilder.buildShell( - (BuildContext context, GoRouterState state, Widget child) { - routeState = StatefulShellRouteState.of(context); - return child; - }); + builder: (BuildContext context, StatefulShellRouteState state, + Widget child) { + routeState = state; + return child; }, branches: [ StatefulShellBranch( @@ -3036,12 +3032,10 @@ void main() { final List routes = [ StatefulShellRoute( - builder: (StatefulShellBuilder shellBuilder) { - return shellBuilder.buildShell( - (BuildContext context, GoRouterState state, Widget child) { - routeState = StatefulShellRouteState.of(context); - return child; - }); + builder: (BuildContext context, StatefulShellRouteState state, + Widget child) { + routeState = state; + return child; }, branches: [ StatefulShellBranch(routes: [ @@ -3111,12 +3105,10 @@ void main() { final List routes = [ StatefulShellRoute( - builder: (StatefulShellBuilder shellBuilder) { - return shellBuilder.buildShell( - (BuildContext context, GoRouterState state, Widget child) { - routeState = StatefulShellRouteState.of(context); - return child; - }); + builder: (BuildContext context, StatefulShellRouteState state, + Widget child) { + routeState = state; + return child; }, branches: [ StatefulShellBranch( @@ -3198,12 +3190,10 @@ void main() { final List routes = [ StatefulShellRoute( - builder: (StatefulShellBuilder shellBuilder) { - return shellBuilder.buildShell( - (BuildContext context, GoRouterState state, Widget child) { - routeState = StatefulShellRouteState.of(context); - return child; - }); + builder: (BuildContext context, StatefulShellRouteState state, + Widget child) { + routeState = state; + return child; }, branches: [ StatefulShellBranch(routes: [ @@ -3259,12 +3249,10 @@ void main() { Text('Common - ${state.extra}'), ), StatefulShellRoute( - builder: (StatefulShellBuilder shellBuilder) { - return shellBuilder.buildShell( - (BuildContext context, GoRouterState state, Widget child) { - routeState = StatefulShellRouteState.of(context); - return child; - }); + builder: (BuildContext context, StatefulShellRouteState state, + Widget child) { + routeState = state; + return child; }, branches: [ StatefulShellBranch(routes: [ @@ -3422,12 +3410,10 @@ void main() { final List routes = [ StatefulShellRoute( - builder: (StatefulShellBuilder shellBuilder) { - return shellBuilder.buildShell( - (BuildContext context, GoRouterState state, Widget child) { - routeState = StatefulShellRouteState.of(context); - return child; - }); + builder: (BuildContext context, StatefulShellRouteState state, + Widget child) { + routeState = state; + return child; }, branches: [ StatefulShellBranch(routes: [ diff --git a/packages/go_router/test/test_helpers.dart b/packages/go_router/test/test_helpers.dart index add586cc2527..1000e60b56c5 100644 --- a/packages/go_router/test/test_helpers.dart +++ b/packages/go_router/test/test_helpers.dart @@ -245,8 +245,7 @@ Future simulateAndroidBackButton(WidgetTester tester) async { .handlePlatformMessage('flutter/navigation', message, (_) {}); } -StatefulShellRouteBuilder mockStatefulShellBuilder = ( - StatefulShellBuilder shellBuilder, -) { - return shellBuilder.buildShell((_, __, Widget child) => child); +StatefulShellRouteBuilder mockStatefulShellBuilder = + (BuildContext context, StatefulShellRouteState state, Widget child) { + return child; }; From ddc71f0218ce96bdc324ea42fc0df94963c91974 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20Lo=CC=88fstrand?= Date: Wed, 8 Mar 2023 14:01:23 +0100 Subject: [PATCH 089/112] Re-introduced proper validation of parent Navigator keys for routes in StatefulShellBranch. --- packages/go_router/lib/src/configuration.dart | 9 ++- .../go_router/test/configuration_test.dart | 77 +++++++++++++++++++ 2 files changed, 85 insertions(+), 1 deletion(-) diff --git a/packages/go_router/lib/src/configuration.dart b/packages/go_router/lib/src/configuration.dart index 1f13e12155af..f9ae29979b9c 100644 --- a/packages/go_router/lib/src/configuration.dart +++ b/packages/go_router/lib/src/configuration.dart @@ -96,8 +96,15 @@ class RouteConfiguration { !allowedKeys.contains(branch.navigatorKey), 'StatefulShellBranch must not reuse an ancestor navigatorKey ' '(${branch.navigatorKey})'); + + _debugCheckParentNavigatorKeys( + branch.routes, + >[ + ...allowedKeys, + branch.navigatorKey, + ], + ); } - _debugCheckParentNavigatorKeys(route.routes, allowedKeys); } } return true; diff --git a/packages/go_router/test/configuration_test.dart b/packages/go_router/test/configuration_test.dart index 5683aa12762e..bf3125cc26f1 100644 --- a/packages/go_router/test/configuration_test.dart +++ b/packages/go_router/test/configuration_test.dart @@ -82,6 +82,83 @@ void main() { ); }); + test( + 'throws when StatefulShellRoute sub-route uses incorrect parentNavigatorKey', + () { + final GlobalKey root = + GlobalKey(debugLabel: 'root'); + final GlobalKey keyA = + GlobalKey(debugLabel: 'A'); + final GlobalKey keyB = + GlobalKey(debugLabel: 'B'); + + expect( + () { + RouteConfiguration( + navigatorKey: root, + routes: [ + StatefulShellRoute(branches: [ + StatefulShellBranch( + navigatorKey: keyA, + routes: [ + GoRoute( + path: '/a', + builder: _mockScreenBuilder, + routes: [ + GoRoute( + path: 'details', + builder: _mockScreenBuilder, + parentNavigatorKey: keyB), + ]), + ], + ), + ], builder: mockStatefulShellBuilder), + ], + redirectLimit: 10, + topRedirect: (BuildContext context, GoRouterState state) { + return null; + }, + ); + }, + throwsAssertionError, + ); + }); + + test( + 'does not throw when StatefulShellRoute sub-route uses correct parentNavigatorKeys', + () { + final GlobalKey root = + GlobalKey(debugLabel: 'root'); + final GlobalKey keyA = + GlobalKey(debugLabel: 'A'); + + RouteConfiguration( + navigatorKey: root, + routes: [ + StatefulShellRoute(branches: [ + StatefulShellBranch( + navigatorKey: keyA, + routes: [ + GoRoute( + path: '/a', + builder: _mockScreenBuilder, + routes: [ + GoRoute( + path: 'details', + builder: _mockScreenBuilder, + parentNavigatorKey: keyA), + ]), + ], + ), + ], builder: mockStatefulShellBuilder), + ], + redirectLimit: 10, + topRedirect: (BuildContext context, GoRouterState state) { + return null; + }, + ); + }); + test( 'throws when a sub-route of StatefulShellRoute has a parentNavigatorKey', () { From 01cce043d343832873b948a6493d7974f4833f50 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20Lo=CC=88fstrand?= Date: Wed, 8 Mar 2023 14:11:17 +0100 Subject: [PATCH 090/112] Updated constructors in sample code to use super parameters. --- .../example/lib/stateful_shell_route.dart | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/packages/go_router/example/lib/stateful_shell_route.dart b/packages/go_router/example/lib/stateful_shell_route.dart index d5796febfdb0..d795ec9a6472 100644 --- a/packages/go_router/example/lib/stateful_shell_route.dart +++ b/packages/go_router/example/lib/stateful_shell_route.dart @@ -28,7 +28,7 @@ void main() { /// An example demonstrating how to use nested navigators class NestedTabNavigationExampleApp extends StatelessWidget { /// Creates a NestedTabNavigationExampleApp - NestedTabNavigationExampleApp({Key? key}) : super(key: key); + NestedTabNavigationExampleApp({super.key}); final GoRouter _router = GoRouter( navigatorKey: _rootNavigatorKey, @@ -248,8 +248,7 @@ class RootScreen extends StatelessWidget { {required this.label, required this.detailsPath, this.secondDetailsPath, - Key? key}) - : super(key: key); + super.key}); /// The label final String label; @@ -349,8 +348,8 @@ class DetailsScreen extends StatefulWidget { this.param, this.extra, this.withScaffold = true, - Key? key, - }) : super(key: key); + super.key, + }); /// The label to display in the center of the screen. final String label; @@ -432,7 +431,7 @@ class DetailsScreenState extends State { /// Widget for a modal screen. class ModalScreen extends StatelessWidget { /// Creates a ModalScreen - const ModalScreen({Key? key}) : super(key: key); + const ModalScreen({super.key}); @override Widget build(BuildContext context) { @@ -463,8 +462,7 @@ class ModalScreen extends StatelessWidget { class TabbedRootScreen extends StatelessWidget { /// Constructs a TabbedRootScreen const TabbedRootScreen( - {required this.shellState, required this.children, Key? key}) - : super(key: key); + {required this.shellState, required this.children, super.key}); /// The current state of the parent StatefulShellRoute. final StatefulShellRouteState shellState; @@ -502,8 +500,7 @@ class TabbedRootScreen extends StatelessWidget { /// Widget for the pages in the top tab bar. class TabScreen extends StatelessWidget { /// Creates a RootScreen - const TabScreen({required this.label, this.detailsPath, Key? key}) - : super(key: key); + const TabScreen({required this.label, this.detailsPath, super.key}); /// The label final String label; From 93153735349981eae5cf2483eb3860599e4e2034 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20Lo=CC=88fstrand?= Date: Fri, 10 Mar 2023 14:29:30 +0100 Subject: [PATCH 091/112] Removed preload support. Various refactoring due to review feedback. --- .../example/lib/stateful_shell_route.dart | 4 +- packages/go_router/lib/go_router.dart | 2 +- packages/go_router/lib/src/builder.dart | 59 +--- packages/go_router/lib/src/route.dart | 292 +++++------------- packages/go_router/lib/src/typedefs.dart | 4 + packages/go_router/test/go_router_test.dart | 102 ------ 6 files changed, 97 insertions(+), 366 deletions(-) diff --git a/packages/go_router/example/lib/stateful_shell_route.dart b/packages/go_router/example/lib/stateful_shell_route.dart index d795ec9a6472..2716c401892d 100644 --- a/packages/go_router/example/lib/stateful_shell_route.dart +++ b/packages/go_router/example/lib/stateful_shell_route.dart @@ -158,13 +158,13 @@ class NestedTabNavigationExampleApp extends StatelessWidget { /// This nested StatefulShellRoute demonstrates the use of a /// custom container (TabBarView) for the branch Navigators. /// Here, the default branch Navigator container (`child`) is - /// ignored, and the class StatefulShellRouteController is + /// ignored, and the class StatefulNavigationShell is /// instead used to provide access to the widgets representing /// the branch Navigators (`List children`). /// /// See TabbedRootScreen for more details on how the children /// are used in the TabBarView. - return StatefulShellRouteController( + return StatefulNavigationShell( shellRouteState: state, containerBuilder: (BuildContext context, StatefulShellRouteState state, diff --git a/packages/go_router/lib/go_router.dart b/packages/go_router/lib/go_router.dart index e6438e255083..dfe9d979c4cb 100644 --- a/packages/go_router/lib/go_router.dart +++ b/packages/go_router/lib/go_router.dart @@ -13,9 +13,9 @@ export 'src/configuration.dart' RouteBase, ShellRoute, ShellNavigatorContainerBuilder, + StatefulNavigationShell, StatefulShellBranch, StatefulShellRoute, - StatefulShellRouteController, StatefulShellRouteState; export 'src/misc/extensions.dart'; export 'src/misc/inherited_router.dart'; diff --git a/packages/go_router/lib/src/builder.dart b/packages/go_router/lib/src/builder.dart index 0cea7b012c29..5c649f92817d 100644 --- a/packages/go_router/lib/src/builder.dart +++ b/packages/go_router/lib/src/builder.dart @@ -273,14 +273,16 @@ class RouteBuilder { ); } - final _ShellNavigatorBuilder shellNavigatorBuilder = - _ShellNavigatorBuilder._( - this, matchList, shellNavigatorKey, buildShellNavigator); + final ShellRouteContext shellRouteContext = ShellRouteContext( + subRoute: subRoute, + routeMatchList: matchList, + navigatorBuilder: buildShellNavigator, + ); // Build the Page for this route final Page page = _buildPageForRoute( context, state, match, pagePopContext, - shellNavigatorBuilder: shellNavigatorBuilder); + shellRouteContext: shellRouteContext); registry[page] = state; // Place the ShellRoute's Page onto the list for the parent navigator. keyToPages @@ -347,7 +349,7 @@ class RouteBuilder { /// Builds a [Page] for [StackedRoute] Page _buildPageForRoute(BuildContext context, GoRouterState state, RouteMatch match, _PagePopContext pagePopContext, - {ShellNavigatorBuilder? shellNavigatorBuilder}) { + {ShellRouteContext? shellRouteContext}) { final RouteBase route = match.route; Page? page; @@ -358,9 +360,9 @@ class RouteBuilder { page = pageBuilder(context, state); } } else if (route is ShellRouteBase) { - assert(shellNavigatorBuilder != null, - 'ShellNavigatorBuilder must be provided for ${route.runtimeType}'); - page = route.buildPage(context, state, shellNavigatorBuilder!); + assert(shellRouteContext != null, + 'ShellRouteContext must be provided for ${route.runtimeType}'); + page = route.buildPage(context, state, shellRouteContext!); } if (page is NoOpPage) { @@ -369,7 +371,7 @@ class RouteBuilder { page ??= buildPage(context, state, Builder(builder: (BuildContext context) { return _callRouteBuilder(context, state, match, - shellNavigatorBuilder: shellNavigatorBuilder); + shellNavigatorBuilder: shellRouteContext); })); pagePopContext._setRouteMatchForPage(page, match); @@ -380,7 +382,7 @@ class RouteBuilder { /// Calls the user-provided route builder from the [RouteMatch]'s [RouteBase]. Widget _callRouteBuilder( BuildContext context, GoRouterState state, RouteMatch match, - {ShellNavigatorBuilder? shellNavigatorBuilder}) { + {ShellRouteContext? shellNavigatorBuilder}) { final RouteBase route = match.route; if (route is GoRoute) { @@ -393,7 +395,7 @@ class RouteBuilder { return builder(context, state); } else if (route is ShellRouteBase) { assert(shellNavigatorBuilder != null, - 'ShellNavigatorBuilder must be provided for ${route.runtimeType}'); + 'ShellRouteContext must be provided for ${route.runtimeType}'); final Widget? widget = route.buildWidget(context, state, shellNavigatorBuilder!); if (widget == null) { @@ -615,38 +617,3 @@ class _PagePopContext { return routeBuilderOnPopPage(route, result, _routeMatchLookUp[page]); } } - -typedef _NavigatorBuilder = Widget Function( - List? observers, String? restorationScopeId); - -/// Provides support for building Navigators for routes. -class _ShellNavigatorBuilder extends ShellNavigatorBuilder { - _ShellNavigatorBuilder._( - this.routeBuilder, - this.routeMatchList, - this.navigatorKey, - this.navigatorBuilder, - ); - - @override - final RouteBuilder routeBuilder; - - @override - final RouteMatchList routeMatchList; - - @override - final GlobalKey navigatorKey; - - final _NavigatorBuilder navigatorBuilder; - - @override - Widget buildNavigator({ - List? observers, - String? restorationScopeId, - }) { - return navigatorBuilder( - observers, - restorationScopeId, - ); - } -} diff --git a/packages/go_router/lib/src/route.dart b/packages/go_router/lib/src/route.dart index b0aa0de33e70..b3ab943ff313 100644 --- a/packages/go_router/lib/src/route.dart +++ b/packages/go_router/lib/src/route.dart @@ -9,11 +9,9 @@ import 'package:flutter/widgets.dart'; import 'package:meta/meta.dart'; import '../go_router.dart'; -import 'builder.dart'; -import 'match.dart'; import 'matching.dart'; -import 'parser.dart'; import 'path_utils.dart'; +import 'typedefs.dart'; /// The base class for [GoRoute] and [ShellRoute]. /// @@ -336,36 +334,48 @@ 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, - ShellNavigatorBuilder navigatorBuilder); + ShellRouteContext shellRouteContext); /// 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, - ShellNavigatorBuilder navigatorBuilder); + ShellRouteContext shellRouteContext); /// 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); } -/// Navigator builder for shell routes. -abstract class ShellNavigatorBuilder { - /// The [GlobalKey] to be used by the [Navigator] built for the current route. - GlobalKey get navigatorKey; +/// Context object used when building the shell and Navigator for a shell route. +class ShellRouteContext { + /// Constructs a [ShellRouteContext]. + ShellRouteContext({ + required this.subRoute, + required this.routeMatchList, + required this.navigatorBuilder, + }); + + /// The current immediate sub-route of the associated shell route. + final RouteBase subRoute; /// The route match list for the current route. - RouteMatchList get routeMatchList; + final RouteMatchList routeMatchList; - /// The route builder. - RouteBuilder get routeBuilder; + /// The navigator builder. + final NavigatorBuilder navigatorBuilder; /// Builds the [Navigator] for the current route. Widget buildNavigator({ List? observers, String? restorationScopeId, - }); + }) { + return navigatorBuilder( + observers, + restorationScopeId, + ); + } } /// A route that displays a UI shell around the matching child route. @@ -501,9 +511,9 @@ class ShellRoute extends ShellRouteBase { @override Widget? buildWidget(BuildContext context, GoRouterState state, - ShellNavigatorBuilder navigatorBuilder) { + ShellRouteContext shellRouteContext) { if (builder != null) { - final Widget navigator = navigatorBuilder.buildNavigator( + final Widget navigator = shellRouteContext.buildNavigator( observers: observers, restorationScopeId: restorationScopeId); return builder!(context, state, navigator); } @@ -512,9 +522,9 @@ class ShellRoute extends ShellRouteBase { @override Page? buildPage(BuildContext context, GoRouterState state, - ShellNavigatorBuilder navigatorBuilder) { + ShellRouteContext shellRouteContext) { if (pageBuilder != null) { - final Widget navigator = navigatorBuilder.buildNavigator( + final Widget navigator = shellRouteContext.buildNavigator( observers: observers, restorationScopeId: restorationScopeId); return pageBuilder!(context, state, navigator); } @@ -583,7 +593,7 @@ class ShellRoute extends ShellRouteBase { /// Sometimes greater control is needed over the layout and animations of the /// Widgets representing the branch Navigators. In such cases, a custom /// implementation can choose to ignore the child parameter of the builders and -/// instead create a [StatefulShellRouteController], which will manage the state +/// instead create a [StatefulNavigationShell], which will manage the state /// of the StatefulShellRoute. When creating this controller, a builder function /// is provided to create the container Widget for the branch Navigators. See /// [ShellNavigatorContainerBuilder] for more details. @@ -690,7 +700,7 @@ class StatefulShellRoute extends ShellRouteBase { /// (using [StatefulShellRouteState.goBranch]). /// /// Custom implementations may choose to ignore the child parameter passed to - /// the builder function, and instead use [StatefulShellRouteController] to + /// the builder function, and instead use [StatefulNavigationShell] to /// create a custom container for the branch Navigators. final StatefulShellRouteBuilder? builder; @@ -707,7 +717,7 @@ class StatefulShellRoute extends ShellRouteBase { /// (using [StatefulShellRouteState.goBranch]). /// /// Custom implementations may choose to ignore the child parameter passed to - /// the builder function, and instead use [StatefulShellRouteController] to + /// the builder function, and instead use [StatefulNavigationShell] to /// create a custom container for the branch Navigators. final StatefulShellRoutePageBuilder? pageBuilder; @@ -720,10 +730,10 @@ class StatefulShellRoute extends ShellRouteBase { @override Widget? buildWidget(BuildContext context, GoRouterState state, - ShellNavigatorBuilder navigatorBuilder) { + ShellRouteContext shellRouteContext) { if (builder != null) { - final _StatefulNavigationShell shell = - _createShell(context, state, navigatorBuilder); + final StatefulNavigationShell shell = + _createShell(context, state, shellRouteContext); return builder!(context, shell.shellRouteState, shell); } return null; @@ -731,10 +741,10 @@ class StatefulShellRoute extends ShellRouteBase { @override Page? buildPage(BuildContext context, GoRouterState state, - ShellNavigatorBuilder navigatorBuilder) { + ShellRouteContext shellRouteContext) { if (pageBuilder != null) { - final _StatefulNavigationShell shell = - _createShell(context, state, navigatorBuilder); + final StatefulNavigationShell shell = + _createShell(context, state, shellRouteContext); return pageBuilder!(context, shell.shellRouteState, shell); } return null; @@ -748,11 +758,13 @@ class StatefulShellRoute extends ShellRouteBase { return branch!.navigatorKey; } - _StatefulNavigationShell _createShell(BuildContext context, - GoRouterState state, ShellNavigatorBuilder navigatorBuilder) { - final _StatefulShellRouteState shellRouteState = - _StatefulShellRouteState._(this, state, navigatorBuilder); - return _StatefulNavigationShell._(shellRouteState); + StatefulNavigationShell _createShell(BuildContext context, + GoRouterState state, ShellRouteContext shellRouteContext) { + final GlobalKey navigatorKey = + navigatorKeyForSubRoute(shellRouteContext.subRoute); + final StatefulShellRouteState shellRouteState = + StatefulShellRouteState._(this, state, navigatorKey, shellRouteContext); + return StatefulNavigationShell(shellRouteState: shellRouteState); } static List _routes(List branches) => @@ -801,7 +813,6 @@ class StatefulShellBranch { this.initialLocation, this.restorationScopeId, this.observers, - this.preload = false, }) : navigatorKey = navigatorKey ?? GlobalKey(); /// The [GlobalKey] to be used by the [Navigator] built for this branch. @@ -824,17 +835,6 @@ class StatefulShellBranch { /// [StatefulShellRouteState]). final String? initialLocation; - /// Whether this route branch should be preloaded when the associated - /// [StatefulShellRoute] is visited for the first time. - /// - /// If this is true, this branch will be preloaded by navigating to - /// the initial location (see [initialLocation]). 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; - /// Restoration ID to save and restore the state of the navigator, including /// its history. final String? restorationScopeId; @@ -845,42 +845,6 @@ class StatefulShellBranch { final List? observers; } -/// The snapshot of the current state of a [StatefulShellRoute]. -/// -/// Note that this an immutable class, that represents the snapshot of the state -/// of a StatefulShellRoute at a given point in time. Therefore, instances of -/// this object should not be cached, but instead passed down from the builder -/// functions of StatefulShellRoute. -@immutable -abstract class StatefulShellRouteState { - /// The associated [StatefulShellRoute] - StatefulShellRoute get route; - - /// The GoRouterState associate with [route]. - GoRouterState get routerState; - - /// The index of the currently active [StatefulShellBranch]. - /// - /// Corresponds to the index of the branch in the List returned from - /// branchBuilder of [StatefulShellRoute]. - int get currentIndex; - - /// The Navigator key of the current navigator. - GlobalKey get currentNavigatorKey; - - /// Navigate to the current location of the shell navigator with the provided - /// index. - /// - /// This method will switch the currently active [Navigator] for the - /// [StatefulShellRoute] by replacing the current navigation stack with the - /// one of the route branch identified by the provided index. If resetLocation - /// is true, the branch will be reset to its initial location - /// (see [StatefulShellBranch.initialLocation]). - void goBranch({ - required int index, - }); -} - /// Builder for a custom container for shell route Navigators. typedef ShellNavigatorContainerBuilder = Widget Function(BuildContext context, StatefulShellRouteState shellRouteState, List children); @@ -889,7 +853,7 @@ typedef ShellNavigatorContainerBuilder = Widget Function(BuildContext context, /// /// 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, StatefulShellRouteController can be used in +/// 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 [ShellNavigatorContainerBuilder], /// where the List of Widgets represent the Navigators for each branch. @@ -897,44 +861,19 @@ typedef ShellNavigatorContainerBuilder = Widget Function(BuildContext context, /// Example: /// ``` /// builder: (BuildContext context, StatefulShellRouteState state, Widget child) { -/// return StatefulShellRouteController( +/// return StatefulNavigationShell( /// shellRouteState: state, /// containerBuilder: (_, __, List children) => MyCustomShell(shellState: state, children: children), /// ); /// } /// ``` -abstract class StatefulShellRouteController extends StatefulWidget { - /// Constructs a [StatefulShellRouteController]. - factory StatefulShellRouteController( - {required StatefulShellRouteState shellRouteState, - required ShellNavigatorContainerBuilder containerBuilder}) => - _StatefulNavigationShell._(shellRouteState as _StatefulShellRouteState, - containerBuilder: containerBuilder); - - const StatefulShellRouteController._(); -} - -/// Widget that manages and maintains the state of a [StatefulShellRoute], -/// including the [Navigator]s of the configured route branches. -/// -/// This widget acts as a wrapper around the builder function specified for the -/// associated StatefulShellRoute, and exposes the state (represented by -/// [StatefulShellRouteState]) to its child widgets with the help of the -/// InheritedWidget [_InheritedStatefulNavigationShell]. The state for each route -/// branch is represented by [StatefulShellBranchState] and can be accessed via the -/// StatefulShellRouteState. -/// -/// By default, this widget creates a container for the branch route Navigators, -/// provided as the child argument to the builder of the StatefulShellRoute. -/// However, implementors can choose to disregard this and use an alternate -/// container around the branch navigators -/// (see [StatefulShellRouteState.children]) instead. -class _StatefulNavigationShell extends StatefulShellRouteController { +class StatefulNavigationShell extends StatefulWidget { /// Constructs an [_StatefulNavigationShell]. - const _StatefulNavigationShell._(this.shellRouteState, - {ShellNavigatorContainerBuilder? containerBuilder}) - : _containerBuilder = containerBuilder ?? _defaultChildBuilder, - super._(); + const StatefulNavigationShell( + {required this.shellRouteState, + ShellNavigatorContainerBuilder? containerBuilder, + super.key}) + : containerBuilder = containerBuilder ?? _defaultChildBuilder; static Widget _defaultChildBuilder(BuildContext context, StatefulShellRouteState shellRouteState, List children) { @@ -942,22 +881,24 @@ class _StatefulNavigationShell extends StatefulShellRouteController { currentIndex: shellRouteState.currentIndex, children: children); } - final _StatefulShellRouteState shellRouteState; + /// The current state of the associated [StatefulShellRoute]. + final StatefulShellRouteState shellRouteState; - final ShellNavigatorContainerBuilder _containerBuilder; + /// The builder for a custom container for shell route Navigators. + final ShellNavigatorContainerBuilder containerBuilder; @override State createState() => _StatefulNavigationShellState(); } /// State for StatefulNavigationShell. -class _StatefulNavigationShellState extends State<_StatefulNavigationShell> { +class _StatefulNavigationShellState extends State { final Map _navigatorCache = {}; final List<_StatefulShellBranchState> _branchStates = <_StatefulShellBranchState>[]; - _StatefulShellRouteState get shellState => widget.shellRouteState; + StatefulShellRouteState get shellState => widget.shellRouteState; StatefulShellRoute get _route => widget.shellRouteState.route; @@ -994,86 +935,14 @@ class _StatefulNavigationShellState extends State<_StatefulNavigationShell> { .findStatefulShellBranchDefaultLocation(branch); } - void _preloadBranches() { - for (int i = 0; i < _branchStates.length; i++) { - if (_branchStates[i].branch.preload && !_branchStates[i].isLoaded) { - _branchStates[i] = _updateBranchState(_branchStates[i], loaded: true); - _preloadBranch(_branchStates[i]) - .then((_StatefulShellBranchState branchState) { - setState(() { - _updateBranchStateList(branchState); - }); - }); - } - } - } - - Future<_StatefulShellBranchState> _preloadBranch( - _StatefulShellBranchState branchState) { - final Future navigator = _preloadBranchNavigator(branchState); - - return navigator.then((Widget? navigator) { - return _updateBranchState( - branchState, - navigator: navigator, - ); - }); - } - - Future _preloadBranchNavigator( - _StatefulShellBranchState branchState) { - final GlobalKey navigatorKey = branchState.navigatorKey; - final String location = _defaultBranchLocation(branchState.branch); - - final RouteBuilder routeBuilder = shellState.navigatorBuilder.routeBuilder; - - // Parse a RouteMatchList from location and handle any redirects - final GoRouteInformationParser parser = - GoRouter.of(context).routeInformationParser; - final Future routeMatchList = - parser.parseRouteInformationWithDependencies( - RouteInformation(location: _defaultBranchLocation(branchState.branch)), - context, - ); - - Widget? buildNavigator(RouteMatchList matchList) { - // Find the index of fromRoute in the match list - final int parentShellRouteIndex = - matchList.matches.indexWhere((RouteMatch e) => e.route == _route); - if (parentShellRouteIndex >= 0) { - final int startIndex = parentShellRouteIndex + 1; - final GlobalKey routeNavigatorKey = - _route.navigatorKeyForSubRoute(matchList.matches[startIndex].route); - assert( - navigatorKey == routeNavigatorKey, - 'Incorrect shell navigator key ' - 'for preloaded match list for location "$location"'); - - return routeBuilder.buildPreloadedNestedNavigator( - context, - matchList, - parentShellRouteIndex + 1, - true, - navigatorKey, - restorationScopeId: branchState.branch.restorationScopeId, - observers: branchState.branch.observers, - ); - } else { - return null; - } - } - - return routeMatchList.then(buildNavigator); - } - void _updateCurrentBranchStateFromWidget() { // Connect the goBranch function in the current StatefulShellRouteState // to the _switchBranch implementation in this class. shellState._switchBranch.complete(_switchBranch); final StatefulShellBranch branch = _route.branches[shellState.currentIndex]; - final ShellNavigatorBuilder navigatorBuilder = shellState.navigatorBuilder; - final Widget currentNavigator = navigatorBuilder.buildNavigator( + final ShellRouteContext shellRouteContext = shellState._shellRouteContext; + final Widget currentNavigator = shellRouteContext.buildNavigator( observers: branch.observers, restorationScopeId: branch.restorationScopeId, ); @@ -1086,7 +955,7 @@ class _StatefulNavigationShellState extends State<_StatefulNavigationShell> { currentBranchState = _updateBranchState( currentBranchState, navigator: currentNavigator, - matchList: navigatorBuilder.routeMatchList.unmodifiableMatchList(), + matchList: shellRouteContext.routeMatchList.unmodifiableMatchList(), ); _updateBranchStateList(currentBranchState); @@ -1158,16 +1027,9 @@ class _StatefulNavigationShellState extends State<_StatefulNavigationShell> { } @override - void didUpdateWidget(covariant _StatefulNavigationShell oldWidget) { + void didUpdateWidget(covariant StatefulNavigationShell oldWidget) { super.didUpdateWidget(oldWidget); _updateCurrentBranchStateFromWidget(); - _preloadBranches(); - } - - @override - void didChangeDependencies() { - super.didChangeDependencies(); - _preloadBranches(); } @override @@ -1176,7 +1038,7 @@ class _StatefulNavigationShellState extends State<_StatefulNavigationShell> { .map((StatefulShellBranch branch) => _BranchNavigatorProxy( branch: branch, navigatorForBranch: _navigatorForBranch)) .toList(); - return widget._containerBuilder(context, shellState, children); + return widget.containerBuilder(context, shellState, children); } } @@ -1233,15 +1095,21 @@ class _IndexedStackedRouteBranchContainer extends StatelessWidget { typedef _BranchSwitcher = void Function(int); -/// Internal [StatefulShellRouteState] implementation. -class _StatefulShellRouteState implements StatefulShellRouteState { +/// The snapshot of the current state of a [StatefulShellRoute]. +/// +/// Note that this an immutable class, that represents the snapshot of the state +/// of a StatefulShellRoute at a given point in time. Therefore, instances of +/// this object should not be cached, but instead passed down from the builder +/// functions of StatefulShellRoute. +@immutable +class StatefulShellRouteState { /// Constructs a [_StatefulShellRouteState]. - _StatefulShellRouteState._( + StatefulShellRouteState._( this.route, this.routerState, - this.navigatorBuilder, - ) : currentIndex = - _indexOfBranchNavigatorKey(route, navigatorBuilder.navigatorKey); + this.currentNavigatorKey, + this._shellRouteContext, + ) : currentIndex = _indexOfBranchNavigatorKey(route, currentNavigatorKey); static int _indexOfBranchNavigatorKey( StatefulShellRoute route, GlobalKey navigatorKey) { @@ -1252,28 +1120,23 @@ class _StatefulShellRouteState implements StatefulShellRouteState { } /// The associated [StatefulShellRoute] - @override final StatefulShellRoute route; /// The current route state associated with the [StatefulShellRoute]. - @override final GoRouterState routerState; - /// The ShellNavigatorBuilder responsible for building the Navigator for the + /// The ShellRouteContext responsible for building the Navigator for the /// current [StatefulShellBranch] - final ShellNavigatorBuilder navigatorBuilder; + final ShellRouteContext _shellRouteContext; /// The index of the currently active [StatefulShellBranch]. /// /// Corresponds to the index of the branch in the List returned from /// branchBuilder of [StatefulShellRoute]. - @override final int currentIndex; /// The Navigator key of the current navigator. - @override - GlobalKey get currentNavigatorKey => - route.branches[currentIndex].navigatorKey; + final GlobalKey currentNavigatorKey; /// Completer for a branch switcher function, that will be populated by /// _StatefulNavigationShellState. @@ -1287,7 +1150,6 @@ class _StatefulShellRouteState implements StatefulShellRouteState { /// one of the route branch identified by the provided index. If resetLocation /// is true, the branch will be reset to its initial location /// (see [StatefulShellBranch.initialLocation]). - @override void goBranch({ required int index, }) { diff --git a/packages/go_router/lib/src/typedefs.dart b/packages/go_router/lib/src/typedefs.dart index 5ba23236af71..de48abd1e761 100644 --- a/packages/go_router/lib/src/typedefs.dart +++ b/packages/go_router/lib/src/typedefs.dart @@ -57,3 +57,7 @@ typedef GoRouterBuilderWithNav = Widget Function( /// The signature of the redirect callback. typedef GoRouterRedirect = FutureOr Function( BuildContext context, GoRouterState state); + +/// Signature for functions used to build Navigators +typedef NavigatorBuilder = Widget Function( + List? observers, String? restorationScopeId); diff --git a/packages/go_router/test/go_router_test.dart b/packages/go_router/test/go_router_test.dart index 4cc10c70f14d..576e5e0e64ce 100644 --- a/packages/go_router/test/go_router_test.dart +++ b/packages/go_router/test/go_router_test.dart @@ -3299,108 +3299,6 @@ 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(); - final GlobalKey statefulWidgetKeyB = - GlobalKey(); - final GlobalKey statefulWidgetKeyC = - GlobalKey(); - final GlobalKey statefulWidgetKeyD = - GlobalKey(); - final GlobalKey statefulWidgetKeyE = - GlobalKey(); - - final List routes = [ - StatefulShellRoute( - builder: mockStatefulShellBuilder, - 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( - builder: mockStatefulShellBuilder, - 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, - 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, - redirect: (_, GoRouterState state) { - if (state.location == '/e') { - return '/e/details'; - } - return null; - }, - ); - 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( 'Redirects are correctly handled when switching branch in a ' 'StatefulShellRoute', (WidgetTester tester) async { From 2c298cd6d690273ea46ac043206cbf96140e334f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20Lo=CC=88fstrand?= Date: Mon, 13 Mar 2023 19:37:19 +0100 Subject: [PATCH 092/112] Refactored state management for StatefulShellRoute. Removed some obsolete code. --- .../example/lib/stateful_shell_route.dart | 8 - packages/go_router/lib/src/builder.dart | 62 ++-- packages/go_router/lib/src/configuration.dart | 13 +- packages/go_router/lib/src/matching.dart | 31 +- packages/go_router/lib/src/parser.dart | 18 +- packages/go_router/lib/src/route.dart | 291 +++++++----------- packages/go_router/lib/src/router.dart | 26 +- packages/go_router/test/go_router_test.dart | 96 ++++++ 8 files changed, 291 insertions(+), 254 deletions(-) diff --git a/packages/go_router/example/lib/stateful_shell_route.dart b/packages/go_router/example/lib/stateful_shell_route.dart index 2716c401892d..2460f7dddd8e 100644 --- a/packages/go_router/example/lib/stateful_shell_route.dart +++ b/packages/go_router/example/lib/stateful_shell_route.dart @@ -44,9 +44,6 @@ class NestedTabNavigationExampleApp extends StatelessWidget { branches: [ /// The route branch for the first tab of the bottom navigation bar. StatefulShellBranch( - /// To enable preloading of the initial locations of branches, pass - /// true for the parameter preload. - // preload: true, navigatorKey: _tabANavigatorKey, routes: [ GoRoute( @@ -510,11 +507,6 @@ 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 ('/c1') - 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 5c649f92817d..18b87769123e 100644 --- a/packages/go_router/lib/src/builder.dart +++ b/packages/go_router/lib/src/builder.dart @@ -68,6 +68,10 @@ class RouteBuilder { final Map, HeroController> _goHeroCache = , HeroController>{}; + /// State for any active stateful shell routes + final Map _shellRouteState = + {}; + /// Builds the top-level Navigator for the given [RouteMatchList]. Widget build( BuildContext context, @@ -154,43 +158,19 @@ class RouteBuilder { /// Clean up previous cache to prevent memory leak. _goHeroCache.removeWhere( (GlobalKey key, _) => !keyToPage.keys.contains(key)); - } - } - /// Builds a preloaded nested [Navigator], containing a sub-tree (beginning - /// at startIndex) of the provided route match list. - Widget buildPreloadedNestedNavigator( - BuildContext context, - RouteMatchList matchList, - int startIndex, - bool routerNeglect, - GlobalKey navigatorKey, - {List? observers, - String? restorationScopeId}) { - final Map, List>> keyToPage = - , List>>{}; - try { - final _PagePopContext pagePopContext = _PagePopContext._(onPopPage); - _buildRecursive( - context, - matchList, - startIndex, - pagePopContext, - routerNeglect, - keyToPage, - navigatorKey, , GoRouterState>{}); - - return _buildNavigator( - pagePopContext.onPopPage, - keyToPage[navigatorKey]!, - navigatorKey, - observers: observers, - restorationScopeId: restorationScopeId, - heroController: _getHeroController(context), - ); - } on _RouteBuilderError catch (e) { - return _buildErrorNavigator( - context, e, matchList, onPopPage, configuration.navigatorKey); + /// Clean up cache of shell route states, but keep states for shell routes + /// in the current match list, as well as in any nested stateful shell + /// routes + if (_shellRouteState.isNotEmpty) { + Iterable shellRoutes = matchList.matches + .map((RouteMatch e) => e.route) + .whereType(); + shellRoutes = RouteConfiguration.routesRecursively(shellRoutes) + .whereType(); + _shellRouteState + .removeWhere((ShellRouteBase key, _) => !shellRoutes.contains(key)); + } } } @@ -273,12 +253,20 @@ class RouteBuilder { ); } - final ShellRouteContext shellRouteContext = ShellRouteContext( + ShellRouteContext shellRouteContext = ShellRouteContext( subRoute: subRoute, routeMatchList: matchList, + shellRouteState: _shellRouteState[route], navigatorBuilder: buildShellNavigator, ); + // Call the ShellRouteBase to create/update the shell route state + final Object? shellRouteState = + route.updateShellState(context, state, shellRouteContext); + _shellRouteState[route] = shellRouteState; + shellRouteContext = + shellRouteContext.copyWith(shellRouteState: shellRouteState); + // Build the Page for this route final Page page = _buildPageForRoute( context, state, match, pagePopContext, diff --git a/packages/go_router/lib/src/configuration.dart b/packages/go_router/lib/src/configuration.dart index f9ae29979b9c..9142f8b9d554 100644 --- a/packages/go_router/lib/src/configuration.dart +++ b/packages/go_router/lib/src/configuration.dart @@ -167,22 +167,23 @@ class RouteConfiguration { return true; } - static Iterable _subRoutesRecursively( - List routes) sync* { + /// Returns an Iterable that traverses the provided routes and their + /// sub-routes recursively. + static Iterable routesRecursively( + Iterable routes) sync* { for (final RouteBase route in routes) { yield route; - yield* _subRoutesRecursively(route.routes); + yield* routesRecursively(route.routes); } } static GoRoute? _findFirstGoRoute(List routes) => - _subRoutesRecursively(routes).whereType().firstOrNull; + routesRecursively(routes).whereType().firstOrNull; /// Tests if a route is a descendant of, or same as, an ancestor route. bool _debugIsDescendantOrSame( {required RouteBase ancestor, required RouteBase route}) => - ancestor == route || - _subRoutesRecursively(ancestor.routes).contains(route); + ancestor == route || routesRecursively(ancestor.routes).contains(route); /// Recursively traverses the routes of the provided StatefulShellBranch to /// find the first GoRoute, from which a full path will be derived. diff --git a/packages/go_router/lib/src/matching.dart b/packages/go_router/lib/src/matching.dart index 6577518aa4bc..32dfa71063eb 100644 --- a/packages/go_router/lib/src/matching.dart +++ b/packages/go_router/lib/src/matching.dart @@ -154,20 +154,25 @@ class RouteMatchList { class UnmodifiableRouteMatchList { /// UnmodifiableRouteMatchList constructor. UnmodifiableRouteMatchList.from(RouteMatchList routeMatchList) - : _matches = List.unmodifiable(routeMatchList.matches), - _uri = routeMatchList.uri, - _pathParameters = + : matches = List.unmodifiable(routeMatchList.matches), + uri = routeMatchList.uri, + pathParameters = Map.unmodifiable(routeMatchList.pathParameters); - final List _matches; - final Uri _uri; - final Map _pathParameters; + /// The route matches. + final List matches; + + /// The uri of the current match. + final Uri uri; + + /// Parameters for the matched route, URI-encoded. + final Map pathParameters; /// Creates a new [RouteMatchList] from this UnmodifiableRouteMatchList. RouteMatchList get modifiableMatchList => RouteMatchList( - List.from(_matches), - _uri, - Map.from(_pathParameters)); + List.from(matches), + uri, + Map.from(pathParameters)); @override bool operator ==(Object other) { @@ -177,13 +182,13 @@ class UnmodifiableRouteMatchList { if (other is! UnmodifiableRouteMatchList) { return false; } - return listEquals(other._matches, _matches) && - other._uri == _uri && - mapEquals(other._pathParameters, _pathParameters); + return listEquals(other.matches, matches) && + other.uri == uri && + mapEquals(other.pathParameters, pathParameters); } @override - int get hashCode => Object.hash(_matches, _uri, _pathParameters); + int get hashCode => Object.hash(matches, uri, pathParameters); } /// An error that occurred during matching. diff --git a/packages/go_router/lib/src/parser.dart b/packages/go_router/lib/src/parser.dart index d98f91aeb9fc..954ce90a86ca 100644 --- a/packages/go_router/lib/src/parser.dart +++ b/packages/go_router/lib/src/parser.dart @@ -56,8 +56,12 @@ class GoRouteInformationParser extends RouteInformationParser { ) { late final RouteMatchList initialMatches; try { - initialMatches = matcher.findMatch(routeInformation.location!, - extra: routeInformation.state); + if (routeInformation is PreParsedRouteInformation) { + initialMatches = routeInformation.matchlist; + } else { + initialMatches = matcher.findMatch(routeInformation.location!, + extra: routeInformation.state); + } } on MatcherError { log.info('No initial matches: ${routeInformation.location}'); @@ -124,3 +128,13 @@ class GoRouteInformationParser extends RouteInformationParser { ); } } + +/// Pre-parsed [RouteInformation] that contains a [RouteMatchList]. +class PreParsedRouteInformation extends RouteInformation { + /// Creates a [PreParsedRouteInformation]. + PreParsedRouteInformation( + {super.location, super.state, required this.matchlist}); + + /// The pre-parsed [RouteMatchList]. + final RouteMatchList matchlist; +} diff --git a/packages/go_router/lib/src/route.dart b/packages/go_router/lib/src/route.dart index b3ab943ff313..9a4682fa86d7 100644 --- a/packages/go_router/lib/src/route.dart +++ b/packages/go_router/lib/src/route.dart @@ -2,8 +2,6 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'dart:async'; - import 'package:collection/collection.dart'; import 'package:flutter/widgets.dart'; import 'package:meta/meta.dart'; @@ -346,6 +344,14 @@ abstract class ShellRouteBase extends RouteBase { /// 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); + + /// Creates or updates an optional state object to be associated with an + /// active shell route. + /// + /// The existing state can be accessed via [ShellRouteContext.shellRouteState]. + Object? updateShellState(BuildContext context, GoRouterState state, + ShellRouteContext shellRouteContext) => + null; } /// Context object used when building the shell and Navigator for a shell route. @@ -354,15 +360,32 @@ class ShellRouteContext { ShellRouteContext({ required this.subRoute, required this.routeMatchList, + required this.shellRouteState, required this.navigatorBuilder, }); + /// Constructs a copy of this [ShellRouteContext], with updated values for + /// some of the fields. + ShellRouteContext copyWith({ + Object? shellRouteState, + }) { + return ShellRouteContext( + subRoute: subRoute, + routeMatchList: routeMatchList, + shellRouteState: shellRouteState ?? this.shellRouteState, + navigatorBuilder: navigatorBuilder, + ); + } + /// The current immediate sub-route of the associated shell route. final RouteBase subRoute; /// The route match list for the current route. final RouteMatchList routeMatchList; + /// An optional state object associated with the shell route. + final Object? shellRouteState; + /// The navigator builder. final NavigatorBuilder navigatorBuilder; @@ -750,6 +773,29 @@ class StatefulShellRoute extends ShellRouteBase { return null; } + @override + Object? updateShellState(BuildContext context, GoRouterState state, + ShellRouteContext shellRouteContext) { + // Make sure branch state (locations) is copied over from previous state + final StatefulShellRouteState? previousState = + shellRouteContext.shellRouteState as StatefulShellRouteState?; + final Map branchState = + previousState?._branchState ?? + {}; + + final GlobalKey navigatorKey = + navigatorKeyForSubRoute(shellRouteContext.subRoute); + final StatefulShellRouteState shellRouteState = StatefulShellRouteState._( + GoRouter.maybeOf(context), + this, + state, + navigatorKey, + shellRouteContext, + branchState); + + return shellRouteState; + } + @override GlobalKey navigatorKeyForSubRoute(RouteBase subRoute) { final StatefulShellBranch? branch = branches.firstWhereOrNull( @@ -760,11 +806,10 @@ class StatefulShellRoute extends ShellRouteBase { StatefulNavigationShell _createShell(BuildContext context, GoRouterState state, ShellRouteContext shellRouteContext) { - final GlobalKey navigatorKey = - navigatorKeyForSubRoute(shellRouteContext.subRoute); - final StatefulShellRouteState shellRouteState = - StatefulShellRouteState._(this, state, navigatorKey, shellRouteContext); - return StatefulNavigationShell(shellRouteState: shellRouteState); + assert(shellRouteContext.shellRouteState != null); + return StatefulNavigationShell( + shellRouteState: + shellRouteContext.shellRouteState! as StatefulShellRouteState); } static List _routes(List branches) => @@ -893,133 +938,47 @@ class StatefulNavigationShell extends StatefulWidget { /// State for StatefulNavigationShell. class _StatefulNavigationShellState extends State { - final Map _navigatorCache = {}; - - final List<_StatefulShellBranchState> _branchStates = - <_StatefulShellBranchState>[]; + final Map _branchNavigators = {}; StatefulShellRouteState get shellState => widget.shellRouteState; StatefulShellRoute get _route => widget.shellRouteState.route; Widget? _navigatorForBranch(StatefulShellBranch branch) { - return _navigatorCache[branch.navigatorKey]; + return _branchNavigators[branch.navigatorKey]; } void _setNavigatorForBranch(StatefulShellBranch branch, Widget? navigator) { navigator != null - ? _navigatorCache[branch.navigatorKey] = navigator - : _navigatorCache.remove(branch.navigatorKey); - } - - void _switchBranch(int index) { - final GoRouter goRouter = GoRouter.of(context); - final _StatefulShellBranchState branchState = _branchStates[index]; - final RouteMatchList? matchList = - branchState.matchList?.modifiableMatchList; - if (matchList != null) { - goRouter.routeInformationParser - .processRedirection(matchList, context) - .then(goRouter.routerDelegate.setNewRoutePath) - .onError((_, __) => - goRouter.go(_defaultBranchLocation(branchState.branch))); - } else { - goRouter.go(_defaultBranchLocation(branchState.branch)); - } - } - - String _defaultBranchLocation(StatefulShellBranch branch) { - return branch.initialLocation ?? - GoRouter.of(context) - .routeConfiguration - .findStatefulShellBranchDefaultLocation(branch); + ? _branchNavigators[branch.navigatorKey] = navigator + : _branchNavigators.remove(branch.navigatorKey); } void _updateCurrentBranchStateFromWidget() { - // Connect the goBranch function in the current StatefulShellRouteState - // to the _switchBranch implementation in this class. - shellState._switchBranch.complete(_switchBranch); - final StatefulShellBranch branch = _route.branches[shellState.currentIndex]; final ShellRouteContext shellRouteContext = shellState._shellRouteContext; - final Widget currentNavigator = shellRouteContext.buildNavigator( - observers: branch.observers, - restorationScopeId: branch.restorationScopeId, - ); - // Update or create a new StatefulShellBranchState for the current branch - // (i.e. the arguments currently provided to the Widget). - _StatefulShellBranchState currentBranchState = _branchStates.firstWhere( - (_StatefulShellBranchState e) => e.branch == branch, - orElse: () => _StatefulShellBranchState._(branch)); - currentBranchState = _updateBranchState( - currentBranchState, - navigator: currentNavigator, - matchList: shellRouteContext.routeMatchList.unmodifiableMatchList(), - ); - - _updateBranchStateList(currentBranchState); - } - - _StatefulShellBranchState _updateBranchState( - _StatefulShellBranchState branchState, { - Widget? navigator, - UnmodifiableRouteMatchList? matchList, - bool? loaded, - }) { - bool dirty = false; - if (matchList != null) { - dirty = branchState.matchList != matchList; - } - - if (navigator != null) { - // Only update Navigator for branch if matchList is different (i.e. - // dirty == true) or if Navigator didn't already exist - final bool hasExistingNav = - _navigatorForBranch(branchState.branch) != null; - if (!hasExistingNav || dirty) { - dirty = true; - _setNavigatorForBranch(branchState.branch, navigator); - } - } - - final bool isLoaded = - loaded ?? _navigatorForBranch(branchState.branch) != null; - dirty = dirty || isLoaded != branchState.isLoaded; - - if (dirty) { - return branchState._copy( - isLoaded: isLoaded, - matchList: matchList, + /// Create an unmodifiable copy of the current RouteMatchList, to prevent + /// mutations from affecting the copy saved as the current state for this + /// branch. + final UnmodifiableRouteMatchList currentBranchState = + shellRouteContext.routeMatchList.unmodifiableMatchList(); + + final UnmodifiableRouteMatchList? previousBranchState = + shellState._stateForBranch(branch); + final bool hasExistingNavigator = _navigatorForBranch(branch) != null; + + /// Only update the Navigator of the route match list has changed + if (previousBranchState != currentBranchState || !hasExistingNavigator) { + final Widget currentNavigator = shellRouteContext.buildNavigator( + observers: branch.observers, + restorationScopeId: branch.restorationScopeId, ); - } else { - return branchState; + _setNavigatorForBranch(branch, currentNavigator); + shellState._updateBranchState(branch, currentBranchState); } } - void _updateBranchStateList(_StatefulShellBranchState currentBranchState) { - final List<_StatefulShellBranchState> existingStates = - List<_StatefulShellBranchState>.from(_branchStates); - _branchStates.clear(); - - // Build a new list of the current StatefulShellBranchStates, with an - // updated state for the current branch etc. - for (final StatefulShellBranch branch in _route.branches) { - if (branch.navigatorKey == currentBranchState.navigatorKey) { - _branchStates.add(currentBranchState); - } else { - _branchStates.add(existingStates.firstWhereOrNull( - (_StatefulShellBranchState e) => e.branch == branch) ?? - _StatefulShellBranchState._(branch)); - } - } - - // Remove any obsolete cached Navigators - final Set validKeys = - _route.branches.map((StatefulShellBranch e) => e.navigatorKey).toSet(); - _navigatorCache.removeWhere((Key key, _) => !validKeys.contains(key)); - } - @override void initState() { super.initState(); @@ -1046,6 +1005,12 @@ typedef _NavigatorForBranch = Widget? Function(StatefulShellBranch); /// Widget that serves as the proxy for a branch Navigator Widget, which /// possibly hasn't been created yet. +/// +/// This Widget hides the logic handling whether a Navigator Widget has been +/// created yet for a branch or not, and at the same time ensures that the same +/// Widget class is consistently passed to the containerBuilder. The latter is +/// important for container implementations that cache child widgets, +/// such as [TabBarView]. class _BranchNavigatorProxy extends StatelessWidget { const _BranchNavigatorProxy({ required this.branch, @@ -1093,8 +1058,6 @@ class _IndexedStackedRouteBranchContainer extends StatelessWidget { } } -typedef _BranchSwitcher = void Function(int); - /// The snapshot of the current state of a [StatefulShellRoute]. /// /// Note that this an immutable class, that represents the snapshot of the state @@ -1105,10 +1068,12 @@ typedef _BranchSwitcher = void Function(int); class StatefulShellRouteState { /// Constructs a [_StatefulShellRouteState]. StatefulShellRouteState._( + this._router, this.route, this.routerState, - this.currentNavigatorKey, + GlobalKey currentNavigatorKey, this._shellRouteContext, + this._branchState, ) : currentIndex = _indexOfBranchNavigatorKey(route, currentNavigatorKey); static int _indexOfBranchNavigatorKey( @@ -1119,6 +1084,9 @@ class StatefulShellRouteState { return index; } + // Nullable only for testability + final GoRouter? _router; + /// The associated [StatefulShellRoute] final StatefulShellRoute route; @@ -1135,78 +1103,31 @@ class StatefulShellRouteState { /// branchBuilder of [StatefulShellRoute]. final int currentIndex; - /// The Navigator key of the current navigator. - final GlobalKey currentNavigatorKey; + /// The current state (route match lists) of the branches of the associated [StatefulShellRoute]. + final Map _branchState; - /// Completer for a branch switcher function, that will be populated by - /// _StatefulNavigationShellState. - final Completer<_BranchSwitcher> _switchBranch = Completer<_BranchSwitcher>(); + void _updateBranchState( + StatefulShellBranch branch, UnmodifiableRouteMatchList matchList) => + _branchState[branch] = matchList; - /// Navigate to the current location of the shell navigator with the provided - /// index. - /// - /// This method will switch the currently active [Navigator] for the - /// [StatefulShellRoute] by replacing the current navigation stack with the - /// one of the route branch identified by the provided index. If resetLocation - /// is true, the branch will be reset to its initial location - /// (see [StatefulShellBranch.initialLocation]). - void goBranch({ - required int index, - }) { - assert(index >= 0 && index < route.branches.length); - _switchBranch.future - .then((_BranchSwitcher switchBranch) => switchBranch(index)); - } -} + UnmodifiableRouteMatchList? _stateForBranch(StatefulShellBranch branch) => + _branchState[branch]; -/// The snapshot of the current state for a particular route branch -/// ([StatefulShellBranch]) in a [StatefulShellRoute]. -/// -/// Note that this an immutable class, that represents the snapshot of the state -/// of a StatefulShellBranchState at a given point in time. -@immutable -class _StatefulShellBranchState { - /// Constructs a [StatefulShellBranchState]. - const _StatefulShellBranchState._( - this.branch, { - this.isLoaded = false, - this.matchList, - }); - - /// Constructs a copy of this [StatefulShellBranchState], with updated values for - /// some of the fields. - _StatefulShellBranchState _copy( - {bool? isLoaded, UnmodifiableRouteMatchList? matchList}) { - return _StatefulShellBranchState._( - branch, - isLoaded: isLoaded ?? this.isLoaded, - matchList: matchList ?? this.matchList, - ); + /// Gets the [RouteMatchList] representing the current location of the branch + /// with the provided index. + RouteMatchList? matchListForBranch(int index) { + return _stateForBranch(route.branches[index])?.modifiableMatchList; } - /// The associated [StatefulShellBranch] - final StatefulShellBranch branch; - - /// The current navigation stack for the branch. - final UnmodifiableRouteMatchList? matchList; - - /// Returns true if this branch has been loaded (i.e. visited once or - /// pre-loaded). - final bool isLoaded; - - GlobalKey get navigatorKey => branch.navigatorKey; - - @override - bool operator ==(Object other) { - if (identical(other, this)) { - return true; - } - if (other is! _StatefulShellBranchState) { - return false; - } - return other.branch == branch && other.matchList == matchList; + /// 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, this + /// method will navigate to initial location of the branch (see + /// [StatefulShellBranch.initialLocation]). + void goBranch({required int index}) { + assert(_router != null); + _router!.goBranch(index, this); } - - @override - int get hashCode => Object.hash(branch, matchList); } diff --git a/packages/go_router/lib/src/router.dart b/packages/go_router/lib/src/router.dart index cca660b69947..e7ad5380f40f 100644 --- a/packages/go_router/lib/src/router.dart +++ b/packages/go_router/lib/src/router.dart @@ -131,9 +131,6 @@ class GoRouter extends ChangeNotifier implements RouterConfig { GoRouteInformationParser get routeInformationParser => _routeInformationParser; - /// The route configuration. Used for testing. - RouteConfiguration get routeConfiguration => _routeConfiguration; - /// Gets the current location. // TODO(chunhtai): deprecates this once go_router_builder is migrated to // GoRouterState.of. @@ -186,6 +183,29 @@ class GoRouter extends ChangeNotifier implements RouterConfig { RouteInformation(location: location, state: extra); } + /// Navigate to the last location of the [StatefulShellBranch] at the provided + /// index in the associated [StatefulShellBranch]. + /// + /// If the branch has not been visited before, this method will navigate to + /// initial location of the branch. Consider using + /// [StatefulShellRouteState.goBranch] as a more convenient alternative to + /// this method. + void goBranch(int index, StatefulShellRouteState shellState) { + assert(index >= 0 && index < shellState.route.branches.length); + final RouteMatchList? matchlist = shellState.matchListForBranch(index); + if (matchlist != null) { + _routeInformationProvider.value = PreParsedRouteInformation( + location: matchlist.uri.toString(), + state: matchlist.extra, + matchlist: matchlist); + } else { + final StatefulShellBranch branch = shellState.route.branches[index]; + final String initialLocation = branch.initialLocation ?? + _routeConfiguration.findStatefulShellBranchDefaultLocation(branch); + go(initialLocation); + } + } + /// Navigate to a named route w/ optional parameters, e.g. /// `name='person', params={'fid': 'f2', 'pid': 'p1'}` /// Navigate to the named route. diff --git a/packages/go_router/test/go_router_test.dart b/packages/go_router/test/go_router_test.dart index 576e5e0e64ce..6b5782e12551 100644 --- a/packages/go_router/test/go_router_test.dart +++ b/packages/go_router/test/go_router_test.dart @@ -3092,6 +3092,102 @@ void main() { expect(statefulWidgetKey.currentState?.counter, equals(0)); }); + testWidgets('Maintains state for nested StatefulShellRoute', + (WidgetTester tester) async { + final GlobalKey rootNavigatorKey = + GlobalKey(); + final GlobalKey statefulWidgetKey = + GlobalKey(); + StatefulShellRouteState? routeState1; + StatefulShellRouteState? routeState2; + + final List routes = [ + StatefulShellRoute( + builder: (BuildContext context, StatefulShellRouteState state, + Widget child) { + routeState1 = state; + return child; + }, + branches: [ + StatefulShellBranch(routes: [ + StatefulShellRoute( + builder: (BuildContext context, StatefulShellRouteState state, + Widget child) { + routeState2 = state; + return child; + }, + branches: [ + StatefulShellBranch(routes: [ + GoRoute( + path: '/a', + builder: (BuildContext context, GoRouterState state) => + const Text('Screen A'), + routes: [ + GoRoute( + path: 'detailA', + builder: + (BuildContext context, GoRouterState state) => + Column(children: [ + const Text('Screen A Detail'), + DummyStatefulWidget(key: statefulWidgetKey), + ]), + ), + ], + ), + ]), + StatefulShellBranch(routes: [ + GoRoute( + path: '/b', + builder: (BuildContext context, GoRouterState state) => + const Text('Screen B'), + ), + ]), + StatefulShellBranch(routes: [ + GoRoute( + path: '/c', + builder: (BuildContext context, GoRouterState state) => + const Text('Screen C'), + ), + ]), + ]), + ]), + StatefulShellBranch(routes: [ + GoRoute( + path: '/d', + builder: (BuildContext context, GoRouterState state) => + const Text('Screen D'), + ), + ]), + ], + ), + ]; + + await createRouter(routes, tester, + initialLocation: '/a/detailA', navigatorKey: rootNavigatorKey); + statefulWidgetKey.currentState?.increment(); + expect(find.text('Screen A Detail'), findsOneWidget); + routeState2!.goBranch(index: 1); + await tester.pumpAndSettle(); + expect(find.text('Screen B'), findsOneWidget); + + routeState1!.goBranch(index: 1); + await tester.pumpAndSettle(); + expect(find.text('Screen D'), findsOneWidget); + + routeState1!.goBranch(index: 0); + await tester.pumpAndSettle(); + expect(find.text('Screen B'), findsOneWidget); + + routeState2!.goBranch(index: 2); + await tester.pumpAndSettle(); + expect(find.text('Screen C'), findsOneWidget); + + routeState2!.goBranch(index: 0); + await tester.pumpAndSettle(); + expect(find.text('Screen A Detail'), findsOneWidget); + expect(statefulWidgetKey.currentState?.counter, equals(1)); + }); + testWidgets( 'Pops from the correct Navigator in a StatefulShellRoute when the ' 'Android back button is pressed', (WidgetTester tester) async { From a7d419fc0f733a23a1107345614016ab91d9c1bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20Lo=CC=88fstrand?= Date: Fri, 31 Mar 2023 17:18:39 +0200 Subject: [PATCH 093/112] Moved branch state management back into StatefulShellRoute and simplified API. Moved goBranch from GoRouter into StatefulNavigationShellState. Added support for proper state restoration to StatefulShellRoute. Added support for proper state restoration of imperatively pushed routes. Added support for saving and restoring push count for GoRouterDelegate. --- .../ios/Runner/Base.lproj/Main.storyboard | 2 +- .../stateful_shell_state_restoration.dart | 428 +++++++++++++++ .../example/lib/stateful_shell_route.dart | 50 +- packages/go_router/lib/src/builder.dart | 49 +- packages/go_router/lib/src/configuration.dart | 14 + packages/go_router/lib/src/delegate.dart | 114 +++- packages/go_router/lib/src/matching.dart | 157 ++++-- packages/go_router/lib/src/parser.dart | 29 +- packages/go_router/lib/src/route.dart | 303 +++++++---- packages/go_router/lib/src/router.dart | 23 - packages/go_router/test/builder_test.dart | 118 +---- packages/go_router/test/go_router_test.dart | 497 ++++++++++++++++++ packages/go_router/test/match_test.dart | 5 +- packages/go_router/test/matching_test.dart | 34 ++ packages/go_router/test/test_helpers.dart | 72 ++- 15 files changed, 1525 insertions(+), 370 deletions(-) create mode 100644 packages/go_router/example/lib/others/stateful_shell_state_restoration.dart diff --git a/packages/go_router/example/ios/Runner/Base.lproj/Main.storyboard b/packages/go_router/example/ios/Runner/Base.lproj/Main.storyboard index f3c28516fb38..bb612647feec 100644 --- a/packages/go_router/example/ios/Runner/Base.lproj/Main.storyboard +++ b/packages/go_router/example/ios/Runner/Base.lproj/Main.storyboard @@ -8,7 +8,7 @@ - + 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 new file mode 100644 index 000000000000..5dbea3632bf3 --- /dev/null +++ b/packages/go_router/example/lib/others/stateful_shell_state_restoration.dart @@ -0,0 +1,428 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:collection/collection.dart'; +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; + +final GlobalKey _rootNavigatorKey = + GlobalKey(debugLabel: 'root'); +final GlobalKey _tabANavigatorKey = + GlobalKey(debugLabel: 'tabANav'); + +void main() => runApp(RestorableStatefulShellRouteExampleApp()); + +/// An example demonstrating how to use StatefulShellRoute with state +/// restoration. +class RestorableStatefulShellRouteExampleApp extends StatelessWidget { + /// Creates a NestedTabNavigationExampleApp + RestorableStatefulShellRouteExampleApp({super.key}); + + final GoRouter _router = GoRouter( + navigatorKey: _rootNavigatorKey, + initialLocation: '/a', + restorationScopeId: 'router', + routes: [ + StatefulShellRoute( + restorationScopeId: 'shell1', + branches: [ + /// The route branch for the first tab of the bottom navigation bar. + StatefulShellBranch( + navigatorKey: _tabANavigatorKey, + restorationScopeId: 'branchA', + routes: [ + GoRoute( + /// The screen to display as the root in the first tab of the + /// bottom navigation bar. + path: '/a', + pageBuilder: (BuildContext context, GoRouterState state) => + const MaterialPage( + restorationId: 'screenA', + child: + RootScreen(label: 'A', detailsPath: '/a/details')), + routes: [ + /// The details screen to display stacked on navigator of the + /// first tab. This will cover screen A but not the application + /// shell (bottom navigation bar). + GoRoute( + path: 'details', + pageBuilder: (BuildContext context, GoRouterState state) => + MaterialPage( + restorationId: 'screenADetail', + child: + DetailsScreen(label: 'A', extra: state.extra)), + ), + ], + ), + ], + ), + + /// The route branch for the third tab of the bottom navigation bar. + StatefulShellBranch( + restorationScopeId: 'branchB', + routes: [ + StatefulShellRoute( + restorationScopeId: 'shell2', + + /// This bottom tab uses a nested shell, wrapping sub routes in a + /// top TabBar. + branches: [ + StatefulShellBranch( + restorationScopeId: 'branchB1', + routes: [ + GoRoute( + path: '/b1', + pageBuilder: + (BuildContext context, GoRouterState state) => + const MaterialPage( + restorationId: 'screenB1', + child: TabScreen( + label: 'B1', + detailsPath: '/b1/details')), + routes: [ + GoRoute( + path: 'details', + pageBuilder: + (BuildContext context, GoRouterState state) => + MaterialPage( + restorationId: 'screenB1Detail', + child: DetailsScreen( + label: 'B1', + extra: state.extra, + withScaffold: false, + )), + ), + ], + ), + ]), + StatefulShellBranch( + restorationScopeId: 'branchB2', + routes: [ + GoRoute( + path: '/b2', + pageBuilder: + (BuildContext context, GoRouterState state) => + const MaterialPage( + restorationId: 'screenB2', + child: TabScreen( + label: 'B2', + detailsPath: '/b2/details')), + routes: [ + GoRoute( + path: 'details', + pageBuilder: + (BuildContext context, GoRouterState state) => + MaterialPage( + restorationId: 'screenB2Detail', + child: DetailsScreen( + label: 'B2', + extra: state.extra, + withScaffold: false, + )), + ), + ], + ), + ]), + ], + pageBuilder: (BuildContext context, + StatefulShellRouteState state, Widget child) { + return MaterialPage( + restorationId: 'shellWidget2', + child: StatefulNavigationShell( + shellRouteState: state, + containerBuilder: (BuildContext context, + StatefulShellRouteState state, + List children) => + TabbedRootScreen( + shellState: state, children: children), + )); + }, + ), + ], + ), + ], + pageBuilder: (BuildContext context, StatefulShellRouteState state, + Widget child) { + return MaterialPage( + restorationId: 'shellWidget1', + child: ScaffoldWithNavBar(shellState: state, body: child)); + }, + ), + ], + ); + + @override + Widget build(BuildContext context) { + return MaterialApp.router( + restorationScopeId: 'app', + title: 'Flutter Demo', + theme: ThemeData( + primarySwatch: Colors.blue, + ), + routerConfig: _router, + ); + } +} + +/// Builds the "shell" for the app by building a Scaffold with a +/// BottomNavigationBar, where [child] is placed in the body of the Scaffold. +class ScaffoldWithNavBar extends StatelessWidget { + /// Constructs an [ScaffoldWithNavBar]. + const ScaffoldWithNavBar({ + required this.shellState, + required this.body, + Key? key, + }) : super(key: key ?? const ValueKey('ScaffoldWithNavBar')); + + /// The current state of the parent StatefulShellRoute. + final StatefulShellRouteState shellState; + + /// Body, i.e. the container for the branch Navigators. + final Widget body; + + @override + Widget build(BuildContext context) { + return Scaffold( + body: body, + bottomNavigationBar: BottomNavigationBar( + items: const [ + BottomNavigationBarItem(icon: Icon(Icons.home), label: 'Section A'), + BottomNavigationBarItem(icon: Icon(Icons.tab), label: 'Section B'), + ], + currentIndex: shellState.currentIndex, + onTap: (int tappedIndex) => shellState.goBranch(index: tappedIndex), + ), + ); + } +} + +/// Widget for the root/initial pages in the bottom navigation bar. +class RootScreen extends StatelessWidget { + /// Creates a RootScreen + const RootScreen({ + required this.label, + required this.detailsPath, + super.key, + }); + + /// The label + final String label; + + /// The path to the detail page + final String detailsPath; + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text('Tab root - $label'), + ), + body: Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text('Screen $label', + style: Theme.of(context).textTheme.titleLarge), + const Padding(padding: EdgeInsets.all(4)), + TextButton( + onPressed: () { + GoRouter.of(context).go(detailsPath, extra: '$label-XYZ'); + }, + child: const Text('View details'), + ), + ], + ), + ), + ); + } +} + +/// The details screen for either the A or B screen. +class DetailsScreen extends StatefulWidget { + /// Constructs a [DetailsScreen]. + const DetailsScreen({ + required this.label, + this.param, + this.extra, + this.withScaffold = true, + super.key, + }); + + /// The label to display in the center of the screen. + final String label; + + /// Optional param + final String? param; + + /// Optional extra object + final Object? extra; + + /// Wrap in scaffold + final bool withScaffold; + + @override + State createState() => DetailsScreenState(); +} + +/// The state for DetailsScreen +class DetailsScreenState extends State with RestorationMixin { + final RestorableInt _counter = RestorableInt(0); + + @override + String? get restorationId => 'DetailsScreen-${widget.label}'; + + @override + void restoreState(RestorationBucket? oldBucket, bool initialRestore) { + registerForRestoration(_counter, 'counter'); + } + + @override + void dispose() { + super.dispose(); + _counter.dispose(); + } + + @override + Widget build(BuildContext context) { + if (widget.withScaffold) { + return Scaffold( + appBar: AppBar( + title: Text('Details Screen - ${widget.label}'), + ), + body: _build(context), + ); + } else { + return Container( + color: Theme.of(context).scaffoldBackgroundColor, + child: _build(context), + ); + } + } + + Widget _build(BuildContext context) { + return Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text('Details for ${widget.label} - Counter: ${_counter.value}', + style: Theme.of(context).textTheme.titleLarge), + const Padding(padding: EdgeInsets.all(4)), + TextButton( + onPressed: () { + setState(() { + _counter.value++; + }); + }, + child: const Text('Increment counter'), + ), + const Padding(padding: EdgeInsets.all(8)), + if (widget.param != null) + Text('Parameter: ${widget.param!}', + style: Theme.of(context).textTheme.titleMedium), + const Padding(padding: EdgeInsets.all(8)), + if (widget.extra != null) + Text('Extra: ${widget.extra!}', + style: Theme.of(context).textTheme.titleMedium), + if (!widget.withScaffold) ...[ + const Padding(padding: EdgeInsets.all(16)), + TextButton( + onPressed: () { + GoRouter.of(context).pop(); + }, + child: const Text('< Back', + style: TextStyle(fontWeight: FontWeight.bold, fontSize: 18)), + ), + ] + ], + ), + ); + } +} + +/// Builds a nested shell using a [TabBar] and [TabBarView]. +class TabbedRootScreen extends StatefulWidget { + /// Constructs a TabbedRootScreen + const TabbedRootScreen( + {required this.shellState, required this.children, super.key}); + + /// The current state of the parent StatefulShellRoute. + final StatefulShellRouteState shellState; + + /// The children (Navigators) to display in the [TabBarView]. + final List children; + + @override + State createState() => _TabbedRootScreenState(); +} + +class _TabbedRootScreenState extends State + with SingleTickerProviderStateMixin { + late final TabController _tabController = TabController( + length: widget.children.length, + vsync: this, + initialIndex: widget.shellState.currentIndex); + + @override + void didUpdateWidget(covariant TabbedRootScreen oldWidget) { + super.didUpdateWidget(oldWidget); + _tabController.index = widget.shellState.currentIndex; + } + + @override + Widget build(BuildContext context) { + final List tabs = widget.children + .mapIndexed((int i, _) => Tab(text: 'Tab ${i + 1}')) + .toList(); + + return Scaffold( + appBar: AppBar( + title: const Text('Tab root'), + bottom: TabBar( + controller: _tabController, + tabs: tabs, + onTap: (int tappedIndex) => _onTabTap(context, tappedIndex), + )), + body: TabBarView( + controller: _tabController, + children: widget.children, + ), + ); + } + + void _onTabTap(BuildContext context, int index) { + widget.shellState.goBranch(index: index); + } +} + +/// Widget for the pages in the top tab bar. +class TabScreen extends StatelessWidget { + /// Creates a RootScreen + const TabScreen({required this.label, this.detailsPath, super.key}); + + /// The label + final String label; + + /// The path to the detail page + final String? detailsPath; + + @override + Widget build(BuildContext context) { + return Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text('Screen $label', style: Theme.of(context).textTheme.titleLarge), + const Padding(padding: EdgeInsets.all(4)), + if (detailsPath != null) + TextButton( + onPressed: () { + GoRouter.of(context).go(detailsPath!); + }, + child: const Text('View details'), + ), + ], + ), + ); + } +} diff --git a/packages/go_router/example/lib/stateful_shell_route.dart b/packages/go_router/example/lib/stateful_shell_route.dart index 2460f7dddd8e..463b4b778ca8 100644 --- a/packages/go_router/example/lib/stateful_shell_route.dart +++ b/packages/go_router/example/lib/stateful_shell_route.dart @@ -456,7 +456,7 @@ class ModalScreen extends StatelessWidget { } /// Builds a nested shell using a [TabBar] and [TabBarView]. -class TabbedRootScreen extends StatelessWidget { +class TabbedRootScreen extends StatefulWidget { /// Constructs a TabbedRootScreen const TabbedRootScreen( {required this.shellState, required this.children, super.key}); @@ -467,30 +467,46 @@ class TabbedRootScreen extends StatelessWidget { /// The children (Navigators) to display in the [TabBarView]. final List children; + @override + State createState() => _TabbedRootScreenState(); +} + +class _TabbedRootScreenState extends State + with SingleTickerProviderStateMixin { + late final TabController _tabController = TabController( + length: widget.children.length, + vsync: this, + initialIndex: widget.shellState.currentIndex); + + @override + void didUpdateWidget(covariant TabbedRootScreen oldWidget) { + super.didUpdateWidget(oldWidget); + _tabController.index = widget.shellState.currentIndex; + } + @override Widget build(BuildContext context) { - final List tabs = - children.mapIndexed((int i, _) => Tab(text: 'Tab ${i + 1}')).toList(); + final List tabs = widget.children + .mapIndexed((int i, _) => Tab(text: 'Tab ${i + 1}')) + .toList(); - return DefaultTabController( - length: children.length, - initialIndex: shellState.currentIndex, - child: Scaffold( - appBar: AppBar( - title: const Text('Tab root'), - bottom: TabBar( - tabs: tabs, - onTap: (int tappedIndex) => _onTabTap(context, tappedIndex), - )), - body: TabBarView( - children: children, - ), + return Scaffold( + appBar: AppBar( + title: const Text('Tab root'), + bottom: TabBar( + controller: _tabController, + tabs: tabs, + onTap: (int tappedIndex) => _onTabTap(context, tappedIndex), + )), + body: TabBarView( + controller: _tabController, + children: widget.children, ), ); } void _onTabTap(BuildContext context, int index) { - shellState.goBranch(index: index); + widget.shellState.goBranch(index: index); } } diff --git a/packages/go_router/lib/src/builder.dart b/packages/go_router/lib/src/builder.dart index 18b87769123e..29bc297d886c 100644 --- a/packages/go_router/lib/src/builder.dart +++ b/packages/go_router/lib/src/builder.dart @@ -68,10 +68,6 @@ class RouteBuilder { final Map, HeroController> _goHeroCache = , HeroController>{}; - /// State for any active stateful shell routes - final Map _shellRouteState = - {}; - /// Builds the top-level Navigator for the given [RouteMatchList]. Widget build( BuildContext context, @@ -155,25 +151,29 @@ class RouteBuilder { _buildErrorPage(context, e, matchList), ]; } finally { - /// Clean up previous cache to prevent memory leak. + /// Clean up previous cache to prevent memory leak, making sure any nested + /// stateful shell routes for the current match list are kept. + final Iterable matchListShellRoutes = matchList + .matches + .map((RouteMatch e) => e.route) + .whereType(); + + final Set activeKeys = keyToPage.keys.toSet() + ..addAll(_nestedStatefulNavigatorKeys(matchListShellRoutes)); _goHeroCache.removeWhere( - (GlobalKey key, _) => !keyToPage.keys.contains(key)); - - /// Clean up cache of shell route states, but keep states for shell routes - /// in the current match list, as well as in any nested stateful shell - /// routes - if (_shellRouteState.isNotEmpty) { - Iterable shellRoutes = matchList.matches - .map((RouteMatch e) => e.route) - .whereType(); - shellRoutes = RouteConfiguration.routesRecursively(shellRoutes) - .whereType(); - _shellRouteState - .removeWhere((ShellRouteBase key, _) => !shellRoutes.contains(key)); - } + (GlobalKey key, _) => !activeKeys.contains(key)); } } + Set> _nestedStatefulNavigatorKeys( + Iterable routes) { + return RouteConfiguration.routesRecursively(routes) + .whereType() + .expand((StatefulShellRoute e) => + e.branches.map((StatefulShellBranch b) => b.navigatorKey)) + .toSet(); + } + void _buildRecursive( BuildContext context, RouteMatchList matchList, @@ -253,20 +253,13 @@ class RouteBuilder { ); } - ShellRouteContext shellRouteContext = ShellRouteContext( + // Call the ShellRouteBase to create/update the shell route state + final ShellRouteContext shellRouteContext = ShellRouteContext( subRoute: subRoute, routeMatchList: matchList, - shellRouteState: _shellRouteState[route], navigatorBuilder: buildShellNavigator, ); - // Call the ShellRouteBase to create/update the shell route state - final Object? shellRouteState = - route.updateShellState(context, state, shellRouteContext); - _shellRouteState[route] = shellRouteState; - shellRouteContext = - shellRouteContext.copyWith(shellRouteState: shellRouteState); - // Build the Page for this route final Page page = _buildPageForRoute( context, state, match, pagePopContext, diff --git a/packages/go_router/lib/src/configuration.dart b/packages/go_router/lib/src/configuration.dart index 9142f8b9d554..e1f1b3046ed5 100644 --- a/packages/go_router/lib/src/configuration.dart +++ b/packages/go_router/lib/src/configuration.dart @@ -198,6 +198,20 @@ class RouteConfiguration { return initialLocation!; } + /// Returns the effective initial location of a StatefulShellBranch. + /// + /// If the initial location of the branch is null, + /// [findStatefulShellBranchDefaultLocation] is used to calculate the initial + /// location. + String effectiveInitialBranchLocation(StatefulShellBranch branch) { + final String? initialLocation = branch.initialLocation; + if (initialLocation != null) { + return initialLocation; + } else { + return findStatefulShellBranchDefaultLocation(branch); + } + } + static String? _fullPathForRoute( RouteBase targetRoute, String parentFullpath, List routes) { for (final RouteBase route in routes) { diff --git a/packages/go_router/lib/src/delegate.dart b/packages/go_router/lib/src/delegate.dart index 59cb5cf40aeb..c6bb29f011c7 100644 --- a/packages/go_router/lib/src/delegate.dart +++ b/packages/go_router/lib/src/delegate.dart @@ -27,7 +27,9 @@ class GoRouterDelegate extends RouterDelegate required List observers, required this.routerNeglect, String? restorationScopeId, - }) : _configuration = configuration { + }) : _configuration = configuration, + _restorablePropertiesRestorationId = + '${restorationScopeId ?? ''}._GoRouterDelegateRestorableProperties' { builder = RouteBuilder( configuration: configuration, builderWithNav: builderWithNav, @@ -48,7 +50,14 @@ class GoRouterDelegate extends RouterDelegate RouteMatchList _matchList = RouteMatchList.empty; - /// Stores the number of times each route route has been pushed. + final RouteConfiguration _configuration; + + final String _restorablePropertiesRestorationId; + final GlobalKey<_GoRouterDelegateRestorablePropertiesState> + _restorablePropertiesKey = + GlobalKey<_GoRouterDelegateRestorablePropertiesState>(); + + /// Increments the stored number of times each route route has been pushed. /// /// This is used to generate a unique key for each route. /// @@ -59,8 +68,18 @@ class GoRouterDelegate extends RouterDelegate /// 'family/:fid': 2, /// } /// ``` - final Map _pushCounts = {}; - final RouteConfiguration _configuration; + int _incrementPushCount(String path) { + final _GoRouterDelegateRestorablePropertiesState? + restorablePropertiesState = _restorablePropertiesKey.currentState; + assert(restorablePropertiesState != null); + + final Map pushCounts = + restorablePropertiesState!.pushCount.value; + final int count = (pushCounts[path] ?? 0) + 1; + pushCounts[path] = count; + restorablePropertiesState.pushCount.value = pushCounts; + return count; + } _NavigatorStateIterator _createNavigatorStateIterator() => _NavigatorStateIterator(_matchList, navigatorKey.currentState!); @@ -82,15 +101,10 @@ class GoRouterDelegate extends RouterDelegate assert(matches.last.route is! ShellRouteBase); // Remap the pageKey to allow any number of the same page on the stack - final int count = (_pushCounts[matches.fullpath] ?? 0) + 1; - _pushCounts[matches.fullpath] = count; + final int count = _incrementPushCount(matches.fullpath); final ValueKey pageKey = ValueKey('${matches.fullpath}-p$count'); final ImperativeRouteMatch newPageKeyMatch = ImperativeRouteMatch( - route: matches.last.route, - subloc: matches.last.subloc, - extra: matches.last.extra, - error: matches.last.error, pageKey: pageKey, matches: matches, ); @@ -169,10 +183,14 @@ class GoRouterDelegate extends RouterDelegate /// For use by the Router architecture as part of the RouterDelegate. @override Widget build(BuildContext context) { - return builder.build( - context, - _matchList, - routerNeglect, + return _GoRouterDelegateRestorableProperties( + key: _restorablePropertiesKey, + restorationId: _restorablePropertiesRestorationId, + builder: (BuildContext context) => builder.build( + context, + _matchList, + routerNeglect, + ), ); } @@ -274,14 +292,15 @@ class _NavigatorStateIterator extends Iterator { // TODO(chunhtai): Removes this once imperative API no longer insert route match. class ImperativeRouteMatch extends RouteMatch { /// Constructor for [ImperativeRouteMatch]. - const ImperativeRouteMatch({ - required super.route, - required super.subloc, - required super.extra, - required super.error, + ImperativeRouteMatch({ required super.pageKey, required this.matches, - }); + }) : super( + route: matches.last.route, + subloc: matches.last.subloc, + extra: matches.last.extra, + error: matches.last.error, + ); /// The matches that produces this route match. final RouteMatchList matches; @@ -294,9 +313,62 @@ class ImperativeRouteMatch extends RouteMatch { if (other is! ImperativeRouteMatch) { return false; } - return super == this && other.matches == matches; + return super == other && + RouteMatchList.matchListEquals(other.matches, matches); } @override int get hashCode => Object.hash(super.hashCode, matches); } + +class _GoRouterDelegateRestorableProperties extends StatefulWidget { + const _GoRouterDelegateRestorableProperties( + {required super.key, required this.restorationId, required this.builder}); + + final String restorationId; + final WidgetBuilder builder; + + @override + State createState() => + _GoRouterDelegateRestorablePropertiesState(); +} + +class _GoRouterDelegateRestorablePropertiesState + extends State<_GoRouterDelegateRestorableProperties> with RestorationMixin { + final _RestorablePushCount pushCount = _RestorablePushCount(); + + @override + String? get restorationId => widget.restorationId; + + @override + void restoreState(RestorationBucket? oldBucket, bool initialRestore) { + registerForRestoration(pushCount, 'push_count'); + } + + @override + Widget build(BuildContext context) { + return widget.builder(context); + } +} + +class _RestorablePushCount extends RestorableValue> { + @override + Map createDefaultValue() => {}; + + @override + Map fromPrimitives(Object? data) { + if (data is Map) { + return data.map((Object? key, Object? value) => + MapEntry(key! as String, value! as int)); + } + return {}; + } + + @override + Object? toPrimitives() => value; + + @override + void didUpdateValue(Map? oldValue) { + notifyListeners(); + } +} diff --git a/packages/go_router/lib/src/matching.dart b/packages/go_router/lib/src/matching.dart index 32dfa71063eb..29cfa48d287b 100644 --- a/packages/go_router/lib/src/matching.dart +++ b/packages/go_router/lib/src/matching.dart @@ -55,9 +55,14 @@ class RouteMatchList { : _matches = matches, fullpath = _generateFullPath(matches); - /// Creates an immutable clone of this RouteMatchList. - UnmodifiableRouteMatchList unmodifiableMatchList() { - return UnmodifiableRouteMatchList.from(this); + RouteMatchList._( + this._matches, this._uri, this.pathParameters, this.fullpath); + + /// Creates a copy of this RouteMatchList that can be modified without + /// affecting the original. + RouteMatchList clone() { + return RouteMatchList._(List.from(_matches), _uri, + Map.from(pathParameters), fullpath); } /// Constructs an empty matches object. @@ -146,49 +151,125 @@ class RouteMatchList { String toString() { return '${objectRuntimeType(this, 'RouteMatchList')}($fullpath)'; } -} - -/// Unmodifiable version of [RouteMatchList] that also supports equality -/// checking based on data. -@immutable -class UnmodifiableRouteMatchList { - /// UnmodifiableRouteMatchList constructor. - UnmodifiableRouteMatchList.from(RouteMatchList routeMatchList) - : matches = List.unmodifiable(routeMatchList.matches), - uri = routeMatchList.uri, - pathParameters = - Map.unmodifiable(routeMatchList.pathParameters); - - /// The route matches. - final List matches; - /// The uri of the current match. - final Uri uri; - - /// Parameters for the matched route, URI-encoded. - final Map pathParameters; + /// Returns a pre-parsed [RouteInformation], containing a reference to this + /// match list. + RouteInformation toPreParsedRouteInformation() { + return RouteInformation( + location: uri.toString(), + state: this, + ); + } - /// Creates a new [RouteMatchList] from this UnmodifiableRouteMatchList. - RouteMatchList get modifiableMatchList => RouteMatchList( - List.from(matches), - uri, - Map.from(pathParameters)); + /// Attempts to extract a pre-parsed match list from the provided + /// [RouteInformation]. + static RouteMatchList? fromPreParsedRouteInformation( + RouteInformation routeInformation) { + if (routeInformation.state is RouteMatchList) { + return routeInformation.state! as RouteMatchList; + } + return null; + } - @override - bool operator ==(Object other) { - if (identical(other, this)) { + /// Performs a deep comparison of two match lists by comparing the fields + /// of each object. + /// + /// Note that the == and hashCode functions are not overridden by + /// RouteMatchList because it is mutable. + static bool matchListEquals(RouteMatchList a, RouteMatchList b) { + if (identical(a, b)) { return true; } - if (other is! UnmodifiableRouteMatchList) { - return false; + return listEquals(a.matches, b.matches) && + a.uri == b.uri && + mapEquals(a.pathParameters, b.pathParameters); + } +} + +/// Handles encoding and decoding of [RouteMatchList] objects to a format +/// suitable for using with [StandardMessageCodec]. +/// +/// The primary use of this class is for state restoration. +class RouteMatchListCodec { + /// Creates a new [RouteMatchListCodec] object. + RouteMatchListCodec(this._matcher); + + static const String _encodedDataKey = 'go_router/encoded_route_match_list'; + static const String _locationKey = 'location'; + static const String _stateKey = 'state'; + static const String _imperativeMatchesKey = 'imperativeMatches'; + static const String _pageKey = 'pageKey'; + + final RouteMatcher _matcher; + + /// Encodes the provided [RouteMatchList]. + Object? encodeMatchList(RouteMatchList matchlist) { + if (matchlist.isEmpty) { + return null; } - return listEquals(other.matches, matches) && - other.uri == uri && - mapEquals(other.pathParameters, pathParameters); + final List> imperativeMatches = matchlist.matches + .whereType() + .map((ImperativeRouteMatch e) => _toPrimitives( + e.matches.uri.toString(), e.extra, + pageKey: e.pageKey.value)) + .toList(); + + return { + _encodedDataKey: _toPrimitives( + matchlist.uri.toString(), matchlist.matches.first.extra, + imperativeMatches: imperativeMatches), + }; } - @override - int get hashCode => Object.hash(matches, uri, pathParameters); + static Map _toPrimitives(String location, Object? state, + {List? imperativeMatches, String? pageKey}) { + return { + _locationKey: location, + _stateKey: state, + if (imperativeMatches != null) _imperativeMatchesKey: imperativeMatches, + if (pageKey != null) _pageKey: pageKey, + }; + } + + /// Attempts to decode the provided object into a [RouteMatchList]. + RouteMatchList? decodeMatchList(Object? object) { + if (object is Map && object[_encodedDataKey] is Map) { + final Map data = + object[_encodedDataKey] as Map; + final Object? rootLocation = data[_locationKey]; + if (rootLocation is! String) { + return null; + } + final RouteMatchList matchList = + _matcher.findMatch(rootLocation, extra: data[_stateKey]); + + final List? imperativeMatches = + data[_imperativeMatchesKey] as List?; + if (imperativeMatches != null) { + for (int i = 0; i < imperativeMatches.length; i++) { + final Object? match = imperativeMatches[i]; + if (match is! Map || + match[_locationKey] is! String || + match[_pageKey] is! String) { + continue; + } + final ValueKey pageKey = + ValueKey(match[_pageKey] as String); + final RouteMatchList imperativeMatchList = _matcher.findMatch( + match[_locationKey] as String, + extra: match[_stateKey]); + final ImperativeRouteMatch imperativeMatch = ImperativeRouteMatch( + pageKey: pageKey, + matches: imperativeMatchList, + ); + matchList.push(imperativeMatch); + } + } + + return matchList; + } + return null; + } } /// An error that occurred during matching. diff --git a/packages/go_router/lib/src/parser.dart b/packages/go_router/lib/src/parser.dart index 954ce90a86ca..ff5e3ce11b29 100644 --- a/packages/go_router/lib/src/parser.dart +++ b/packages/go_router/lib/src/parser.dart @@ -30,6 +30,8 @@ class GoRouteInformationParser extends RouteInformationParser { /// The route matcher. final RouteMatcher matcher; + late final RouteMatchListCodec _matchListCodec = RouteMatchListCodec(matcher); + /// The route redirector. final RouteRedirector redirector; @@ -56,11 +58,16 @@ class GoRouteInformationParser extends RouteInformationParser { ) { late final RouteMatchList initialMatches; try { - if (routeInformation is PreParsedRouteInformation) { - initialMatches = routeInformation.matchlist; + final RouteMatchList? preParsedMatchList = + RouteMatchList.fromPreParsedRouteInformation(routeInformation); + if (preParsedMatchList != null) { + initialMatches = preParsedMatchList; } else { - initialMatches = matcher.findMatch(routeInformation.location!, - extra: routeInformation.state); + final RouteMatchList? decodedMatchList = + _matchListCodec.decodeMatchList(routeInformation.state); + initialMatches = decodedMatchList ?? + matcher.findMatch(routeInformation.location!, + extra: routeInformation.state); } } on MatcherError { log.info('No initial matches: ${routeInformation.location}'); @@ -118,23 +125,15 @@ class GoRouteInformationParser extends RouteInformationParser { if (configuration.isEmpty) { return null; } + final Object? encodedMatchList = + _matchListCodec.encodeMatchList(configuration); if (configuration.matches.last is ImperativeRouteMatch) { configuration = (configuration.matches.last as ImperativeRouteMatch).matches; } return RouteInformation( location: configuration.uri.toString(), - state: configuration.extra, + state: encodedMatchList, ); } } - -/// Pre-parsed [RouteInformation] that contains a [RouteMatchList]. -class PreParsedRouteInformation extends RouteInformation { - /// Creates a [PreParsedRouteInformation]. - PreParsedRouteInformation( - {super.location, super.state, required this.matchlist}); - - /// The pre-parsed [RouteMatchList]. - final RouteMatchList matchlist; -} diff --git a/packages/go_router/lib/src/route.dart b/packages/go_router/lib/src/route.dart index 9a4682fa86d7..55f3c8f3ac02 100644 --- a/packages/go_router/lib/src/route.dart +++ b/packages/go_router/lib/src/route.dart @@ -7,6 +7,7 @@ import 'package:flutter/widgets.dart'; import 'package:meta/meta.dart'; import '../go_router.dart'; +import 'information_provider.dart'; import 'matching.dart'; import 'path_utils.dart'; import 'typedefs.dart'; @@ -344,14 +345,6 @@ abstract class ShellRouteBase extends RouteBase { /// 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); - - /// Creates or updates an optional state object to be associated with an - /// active shell route. - /// - /// The existing state can be accessed via [ShellRouteContext.shellRouteState]. - Object? updateShellState(BuildContext context, GoRouterState state, - ShellRouteContext shellRouteContext) => - null; } /// Context object used when building the shell and Navigator for a shell route. @@ -360,32 +353,15 @@ class ShellRouteContext { ShellRouteContext({ required this.subRoute, required this.routeMatchList, - required this.shellRouteState, required this.navigatorBuilder, }); - /// Constructs a copy of this [ShellRouteContext], with updated values for - /// some of the fields. - ShellRouteContext copyWith({ - Object? shellRouteState, - }) { - return ShellRouteContext( - subRoute: subRoute, - routeMatchList: routeMatchList, - shellRouteState: shellRouteState ?? this.shellRouteState, - navigatorBuilder: navigatorBuilder, - ); - } - /// The current immediate sub-route of the associated shell route. final RouteBase subRoute; /// The route match list for the current route. final RouteMatchList routeMatchList; - /// An optional state object associated with the shell route. - final Object? shellRouteState; - /// The navigator builder. final NavigatorBuilder navigatorBuilder; @@ -702,14 +678,20 @@ class StatefulShellRoute extends ShellRouteBase { required this.branches, this.builder, this.pageBuilder, + this.restorationScopeId, }) : assert(branches.isNotEmpty), assert((pageBuilder != null) ^ (builder != null), '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)); + /// 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 child parameter. This @@ -751,6 +733,9 @@ class StatefulShellRoute extends ShellRouteBase { /// [StatefulShellBranch.navigatorKey]. final List branches; + final GlobalKey _shellStateKey = + GlobalKey(); + @override Widget? buildWidget(BuildContext context, GoRouterState state, ShellRouteContext shellRouteContext) { @@ -773,29 +758,6 @@ class StatefulShellRoute extends ShellRouteBase { return null; } - @override - Object? updateShellState(BuildContext context, GoRouterState state, - ShellRouteContext shellRouteContext) { - // Make sure branch state (locations) is copied over from previous state - final StatefulShellRouteState? previousState = - shellRouteContext.shellRouteState as StatefulShellRouteState?; - final Map branchState = - previousState?._branchState ?? - {}; - - final GlobalKey navigatorKey = - navigatorKeyForSubRoute(shellRouteContext.subRoute); - final StatefulShellRouteState shellRouteState = StatefulShellRouteState._( - GoRouter.maybeOf(context), - this, - state, - navigatorKey, - shellRouteContext, - branchState); - - return shellRouteState; - } - @override GlobalKey navigatorKeyForSubRoute(RouteBase subRoute) { final StatefulShellBranch? branch = branches.firstWhereOrNull( @@ -806,10 +768,17 @@ class StatefulShellRoute extends ShellRouteBase { StatefulNavigationShell _createShell(BuildContext context, GoRouterState state, ShellRouteContext shellRouteContext) { - assert(shellRouteContext.shellRouteState != null); - return StatefulNavigationShell( - shellRouteState: - shellRouteContext.shellRouteState! as StatefulShellRouteState); + final GlobalKey navigatorKey = + navigatorKeyForSubRoute(shellRouteContext.subRoute); + final StatefulShellRouteState shellRouteState = StatefulShellRouteState._( + GoRouter.of(context), + this, + _shellStateKey, + state, + navigatorKey, + shellRouteContext, + ); + return StatefulNavigationShell(shellRouteState: shellRouteState); } static List _routes(List branches) => @@ -832,6 +801,21 @@ class StatefulShellRoute extends ShellRouteBase { } return true; } + + static bool _debugValidateRestorationScopeIds( + String? restorationScopeId, List branches) { + if (branches + .map((StatefulShellBranch e) => e.restorationScopeId) + .whereNotNull() + .isNotEmpty) { + assert( + restorationScopeId != null, + 'A restorationScopeId must be set for ' + 'the StatefulShellRoute when using restorationScopeIds on one or more ' + 'of the branches'); + } + return true; + } } /// Representation of a separate branch in a stateful navigation tree, used to @@ -914,11 +898,11 @@ typedef ShellNavigatorContainerBuilder = Widget Function(BuildContext context, /// ``` class StatefulNavigationShell extends StatefulWidget { /// Constructs an [_StatefulNavigationShell]. - const StatefulNavigationShell( - {required this.shellRouteState, - ShellNavigatorContainerBuilder? containerBuilder, - super.key}) - : containerBuilder = containerBuilder ?? _defaultChildBuilder; + StatefulNavigationShell({ + required this.shellRouteState, + ShellNavigatorContainerBuilder? containerBuilder, + }) : containerBuilder = containerBuilder ?? _defaultChildBuilder, + super(key: shellRouteState._shellStateKey); static Widget _defaultChildBuilder(BuildContext context, StatefulShellRouteState shellRouteState, List children) { @@ -933,17 +917,48 @@ class StatefulNavigationShell extends StatefulWidget { final ShellNavigatorContainerBuilder containerBuilder; @override - State createState() => _StatefulNavigationShellState(); + State createState() => StatefulNavigationShellState(); } /// State for StatefulNavigationShell. -class _StatefulNavigationShellState extends State { +class StatefulNavigationShellState extends State + with RestorationMixin { final Map _branchNavigators = {}; - StatefulShellRouteState get shellState => widget.shellRouteState; - StatefulShellRoute get _route => widget.shellRouteState.route; + StatefulShellRouteState get _routeState => widget.shellRouteState; + + GoRouter get _router => _routeState._router; + RouteMatcher get _matcher => _router.routeInformationParser.matcher; + GoRouteInformationProvider get _routeInformationProvider => + _router.routeInformationProvider; + + final Map _branchLocations = + {}; + + @override + 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 + /// always returned (needed for _RestorableRouteMatchList/RestorableValue). + String _branchLocationRestorationScopeId(StatefulShellBranch branch) { + return branch.restorationScopeId != null + ? '${branch.restorationScopeId}-location' + : identityHashCode(branch).toString(); + } + + _RestorableRouteMatchList _branchLocation(StatefulShellBranch branch) { + return _branchLocations.putIfAbsent(branch, () { + final _RestorableRouteMatchList branchLocation = + _RestorableRouteMatchList(_matcher); + registerForRestoration( + branchLocation, _branchLocationRestorationScopeId(branch)); + return branchLocation; + }); + } + Widget? _navigatorForBranch(StatefulShellBranch branch) { return _branchNavigators[branch.navigatorKey]; } @@ -954,37 +969,65 @@ class _StatefulNavigationShellState extends State { : _branchNavigators.remove(branch.navigatorKey); } - void _updateCurrentBranchStateFromWidget() { - final StatefulShellBranch branch = _route.branches[shellState.currentIndex]; - final ShellRouteContext shellRouteContext = shellState._shellRouteContext; - - /// Create an unmodifiable copy of the current RouteMatchList, to prevent - /// mutations from affecting the copy saved as the current state for this - /// branch. - final UnmodifiableRouteMatchList currentBranchState = - shellRouteContext.routeMatchList.unmodifiableMatchList(); + RouteMatchList? _matchListForBranch(int index) => + _branchLocations[_route.branches[index]]?.value; - final UnmodifiableRouteMatchList? previousBranchState = - shellState._stateForBranch(branch); + void _updateCurrentBranchStateFromWidget() { + final StatefulShellBranch branch = + _route.branches[_routeState.currentIndex]; + final ShellRouteContext shellRouteContext = _routeState._shellRouteContext; + + /// Create an clone of the current RouteMatchList, to prevent mutations from + /// affecting the copy saved as the current state for this branch. + final RouteMatchList currentBranchLocation = + shellRouteContext.routeMatchList.clone(); + + final _RestorableRouteMatchList branchLocation = _branchLocation(branch); + final RouteMatchList previousBranchLocation = branchLocation.value; + branchLocation.value = currentBranchLocation; final bool hasExistingNavigator = _navigatorForBranch(branch) != null; /// Only update the Navigator of the route match list has changed - if (previousBranchState != currentBranchState || !hasExistingNavigator) { + final bool locationChanged = !RouteMatchList.matchListEquals( + previousBranchLocation, currentBranchLocation); + if (locationChanged || !hasExistingNavigator) { final Widget currentNavigator = shellRouteContext.buildNavigator( observers: branch.observers, restorationScopeId: branch.restorationScopeId, ); _setNavigatorForBranch(branch, currentNavigator); - shellState._updateBranchState(branch, currentBranchState); + } + } + + void _goBranch(int index) { + assert(index >= 0 && index < _route.branches.length); + final RouteMatchList? matchlist = _matchListForBranch(index); + if (matchlist != null && matchlist.isNotEmpty) { + _routeInformationProvider.value = matchlist.toPreParsedRouteInformation(); + } else { + _router.go(_routeState._effectiveInitialBranchLocation(index)); } } @override - void initState() { - super.initState(); + void didChangeDependencies() { + super.didChangeDependencies(); _updateCurrentBranchStateFromWidget(); } + @override + void dispose() { + super.dispose(); + for (final StatefulShellBranch branch in _route.branches) { + _branchLocations[branch]?.dispose(); + } + } + + @override + void restoreState(RestorationBucket? oldBucket, bool initialRestore) { + _route.branches.forEach(_branchLocation); + } + @override void didUpdateWidget(covariant StatefulNavigationShell oldWidget) { super.didUpdateWidget(oldWidget); @@ -995,9 +1038,40 @@ class _StatefulNavigationShellState extends State { Widget build(BuildContext context) { final List children = _route.branches .map((StatefulShellBranch branch) => _BranchNavigatorProxy( - branch: branch, navigatorForBranch: _navigatorForBranch)) + key: ObjectKey(branch), + branch: branch, + navigatorForBranch: _navigatorForBranch)) .toList(); - return widget.containerBuilder(context, shellState, children); + return widget.containerBuilder(context, _routeState, children); + } +} + +/// [RestorableProperty] for enabling state restoration of [RouteMatchList]s. +class _RestorableRouteMatchList extends RestorableValue { + _RestorableRouteMatchList(RouteMatcher matcher) + : _matchListCodec = RouteMatchListCodec(matcher); + + final RouteMatchListCodec _matchListCodec; + + @override + RouteMatchList createDefaultValue() => RouteMatchList.empty; + + @override + void didUpdateValue(RouteMatchList? oldValue) { + notifyListeners(); + } + + @override + RouteMatchList fromPrimitives(Object? data) { + return _matchListCodec.decodeMatchList(data) ?? RouteMatchList.empty; + } + + @override + Object? toPrimitives() { + if (value != null && value.isNotEmpty) { + return _matchListCodec.encodeMatchList(value); + } + return null; } } @@ -1011,8 +1085,9 @@ typedef _NavigatorForBranch = Widget? Function(StatefulShellBranch); /// Widget class is consistently passed to the containerBuilder. The latter is /// important for container implementations that cache child widgets, /// such as [TabBarView]. -class _BranchNavigatorProxy extends StatelessWidget { +class _BranchNavigatorProxy extends StatefulWidget { const _BranchNavigatorProxy({ + super.key, required this.branch, required this.navigatorForBranch, }); @@ -1020,10 +1095,23 @@ class _BranchNavigatorProxy extends StatelessWidget { final StatefulShellBranch branch; final _NavigatorForBranch navigatorForBranch; + @override + State createState() => _BranchNavigatorProxyState(); +} + +/// State for _BranchNavigatorProxy, using AutomaticKeepAliveClientMixin to +/// properly handle some scenarios where Slivers are used to manage the branches +/// (such as [TabBarView]). +class _BranchNavigatorProxyState extends State<_BranchNavigatorProxy> + with AutomaticKeepAliveClientMixin { @override Widget build(BuildContext context) { - return navigatorForBranch(branch) ?? const SizedBox.shrink(); + super.build(context); + return widget.navigatorForBranch(widget.branch) ?? const SizedBox.shrink(); } + + @override + bool get wantKeepAlive => true; } /// Default implementation of a container widget for the [Navigator]s of the @@ -1070,10 +1158,10 @@ class StatefulShellRouteState { StatefulShellRouteState._( this._router, this.route, + this._shellStateKey, this.routerState, GlobalKey currentNavigatorKey, this._shellRouteContext, - this._branchState, ) : currentIndex = _indexOfBranchNavigatorKey(route, currentNavigatorKey); static int _indexOfBranchNavigatorKey( @@ -1084,12 +1172,13 @@ class StatefulShellRouteState { return index; } - // Nullable only for testability - final GoRouter? _router; + final GoRouter _router; /// The associated [StatefulShellRoute] final StatefulShellRoute route; + final GlobalKey _shellStateKey; + /// The current route state associated with the [StatefulShellRoute]. final GoRouterState routerState; @@ -1103,22 +1192,6 @@ class StatefulShellRouteState { /// branchBuilder of [StatefulShellRoute]. final int currentIndex; - /// The current state (route match lists) of the branches of the associated [StatefulShellRoute]. - final Map _branchState; - - void _updateBranchState( - StatefulShellBranch branch, UnmodifiableRouteMatchList matchList) => - _branchState[branch] = matchList; - - UnmodifiableRouteMatchList? _stateForBranch(StatefulShellBranch branch) => - _branchState[branch]; - - /// Gets the [RouteMatchList] representing the current location of the branch - /// with the provided index. - RouteMatchList? matchListForBranch(int index) { - return _stateForBranch(route.branches[index])?.modifiableMatchList; - } - /// Navigate to the last location of the [StatefulShellBranch] at the provided /// index in the associated [StatefulShellBranch]. /// @@ -1127,7 +1200,35 @@ class StatefulShellRouteState { /// method will navigate to initial location of the branch (see /// [StatefulShellBranch.initialLocation]). void goBranch({required int index}) { - assert(_router != null); - _router!.goBranch(index, this); + final StatefulNavigationShellState? shellState = + _shellStateKey.currentState; + if (shellState != null) { + shellState._goBranch(index); + } else { + assert(_router != null); + _router.go(_effectiveInitialBranchLocation(index)); + } + } + + String _effectiveInitialBranchLocation(int index) { + return _router.routeInformationParser.configuration + .effectiveInitialBranchLocation(route.branches[index]); + } + + /// Gets the state for the nearest stateful shell route in the Widget tree. + static StatefulShellRouteState of(BuildContext context) { + final StatefulNavigationShellState? shellState = + context.findAncestorStateOfType(); + assert(shellState != null); + return shellState!._routeState; + } + + /// Gets the state for the nearest stateful shell route in the Widget tree. + /// + /// Returns null if no stateful shell route is found. + static StatefulShellRouteState? maybeOf(BuildContext context) { + final StatefulNavigationShellState? shellState = + context.findAncestorStateOfType(); + return shellState?._routeState; } } diff --git a/packages/go_router/lib/src/router.dart b/packages/go_router/lib/src/router.dart index e7ad5380f40f..9ef848b5dd6f 100644 --- a/packages/go_router/lib/src/router.dart +++ b/packages/go_router/lib/src/router.dart @@ -183,29 +183,6 @@ class GoRouter extends ChangeNotifier implements RouterConfig { RouteInformation(location: location, state: extra); } - /// Navigate to the last location of the [StatefulShellBranch] at the provided - /// index in the associated [StatefulShellBranch]. - /// - /// If the branch has not been visited before, this method will navigate to - /// initial location of the branch. Consider using - /// [StatefulShellRouteState.goBranch] as a more convenient alternative to - /// this method. - void goBranch(int index, StatefulShellRouteState shellState) { - assert(index >= 0 && index < shellState.route.branches.length); - final RouteMatchList? matchlist = shellState.matchListForBranch(index); - if (matchlist != null) { - _routeInformationProvider.value = PreParsedRouteInformation( - location: matchlist.uri.toString(), - state: matchlist.extra, - matchlist: matchlist); - } else { - final StatefulShellBranch branch = shellState.route.branches[index]; - final String initialLocation = branch.initialLocation ?? - _routeConfiguration.findStatefulShellBranchDefaultLocation(branch); - go(initialLocation); - } - } - /// Navigate to a named route w/ optional parameters, e.g. /// `name='person', params={'fid': 'f2', 'pid': 'p1'}` /// Navigate to the named route. diff --git a/packages/go_router/test/builder_test.dart b/packages/go_router/test/builder_test.dart index 2231b842897b..0f0c9d20845e 100644 --- a/packages/go_router/test/builder_test.dart +++ b/packages/go_router/test/builder_test.dart @@ -80,8 +80,8 @@ void main() { final RouteMatchList matches = RouteMatchList( [ - _createRouteMatch(config.routes.first, '/'), - _createRouteMatch(config.routes.first.routes.first, '/'), + createRouteMatch(config.routes.first, '/'), + createRouteMatch(config.routes.first.routes.first, '/'), ], Uri.parse('/'), const {}, @@ -97,105 +97,6 @@ void main() { expect(find.byType(_DetailsScreen), findsOneWidget); }); - testWidgets('Builds StatefulShellRoute', (WidgetTester tester) async { - final GlobalKey key = - GlobalKey(debugLabel: 'key'); - final RouteConfiguration config = RouteConfiguration( - routes: [ - StatefulShellRoute( - builder: mockStatefulShellBuilder, - branches: [ - StatefulShellBranch(navigatorKey: key, routes: [ - GoRoute( - path: '/nested', - builder: (BuildContext context, GoRouterState state) { - return _DetailsScreen(); - }, - ), - ]), - ], - ), - ], - redirectLimit: 10, - topRedirect: (_, __) => null, - navigatorKey: GlobalKey(), - ); - - final RouteMatchList matches = RouteMatchList( - [ - _createRouteMatch(config.routes.first, '/nested'), - _createRouteMatch(config.routes.first.routes.first, '/nested'), - ], - Uri.parse('/nested'), - const {}); - - await tester.pumpWidget( - _BuilderTestWidget( - routeConfiguration: config, - matches: matches, - ), - ); - - expect(find.byType(_DetailsScreen), findsOneWidget); - expect(find.byKey(key), findsOneWidget); - }); - - testWidgets('Builds StatefulShellRoute as a sub-route', - (WidgetTester tester) async { - final GlobalKey key = - GlobalKey(debugLabel: 'key'); - late GoRoute root; - late StatefulShellRoute shell; - late GoRoute nested; - final RouteConfiguration config = RouteConfiguration( - routes: [ - root = GoRoute( - path: '/root', - builder: (BuildContext context, GoRouterState state) => - const Text('Root'), - routes: [ - shell = StatefulShellRoute( - builder: mockStatefulShellBuilder, - branches: [ - StatefulShellBranch(navigatorKey: key, routes: [ - nested = GoRoute( - path: 'nested', - builder: (BuildContext context, GoRouterState state) { - return _DetailsScreen(); - }, - ), - ]), - ], - ), - ], - ) - ], - redirectLimit: 10, - topRedirect: (_, __) => null, - navigatorKey: GlobalKey(), - ); - - final RouteMatchList matches = RouteMatchList( - [ - _createRouteMatch(root, '/root'), - _createRouteMatch(shell, 'nested'), - _createRouteMatch(nested, '/root/nested'), - ], - Uri.parse('/root/nested'), - const {}, - ); - - await tester.pumpWidget( - _BuilderTestWidget( - routeConfiguration: config, - matches: matches, - ), - ); - - expect(find.byType(_DetailsScreen), findsOneWidget); - expect(find.byKey(key), findsOneWidget); - }); - testWidgets('Uses the correct navigatorKey', (WidgetTester tester) async { final GlobalKey rootNavigatorKey = GlobalKey(); @@ -404,8 +305,8 @@ void main() { final RouteMatchList matches = RouteMatchList( [ - _createRouteMatch(config.routes.first, ''), - _createRouteMatch(config.routes.first.routes.first, '/a'), + createRouteMatch(config.routes.first, ''), + createRouteMatch(config.routes.first.routes.first, '/a'), ], Uri.parse('/b'), const {}); @@ -435,6 +336,7 @@ void main() { navigatorKey: rootNavigatorKey, routes: [ StatefulShellRoute( + restorationScopeId: 'shell', builder: (BuildContext context, StatefulShellRouteState state, Widget child) => _HomeScreen(child: child), @@ -547,13 +449,3 @@ class _BuilderTestWidget extends StatelessWidget { ); } } - -RouteMatch _createRouteMatch(RouteBase route, String location) { - return RouteMatch( - route: route, - subloc: location, - extra: null, - error: null, - pageKey: ValueKey(location), - ); -} diff --git a/packages/go_router/test/go_router_test.dart b/packages/go_router/test/go_router_test.dart index 6b5782e12551..82c8bdce4b22 100644 --- a/packages/go_router/test/go_router_test.dart +++ b/packages/go_router/test/go_router_test.dart @@ -2929,6 +2929,92 @@ void main() { expect(find.text('Screen C'), findsNothing); }); + testWidgets('Builds StatefulShellRoute', (WidgetTester tester) async { + final GlobalKey rootNavigatorKey = + GlobalKey(); + + final List routes = [ + StatefulShellRoute( + builder: (BuildContext context, StatefulShellRouteState state, + Widget child) => + child, + branches: [ + StatefulShellBranch(routes: [ + GoRoute( + path: '/a', + builder: (BuildContext context, GoRouterState state) => + const Text('Screen A'), + ), + ]), + StatefulShellBranch(routes: [ + GoRoute( + path: '/b', + builder: (BuildContext context, GoRouterState state) => + const Text('Screen B'), + ), + ]), + ], + ), + ]; + + final GoRouter router = await createRouter(routes, tester, + initialLocation: '/a', navigatorKey: rootNavigatorKey); + expect(find.text('Screen A'), findsOneWidget); + expect(find.text('Screen B'), findsNothing); + + router.go('/b'); + await tester.pumpAndSettle(); + expect(find.text('Screen A'), findsNothing); + expect(find.text('Screen B'), findsOneWidget); + }); + + testWidgets('Builds StatefulShellRoute as a sub-route', + (WidgetTester tester) async { + final GlobalKey rootNavigatorKey = + GlobalKey(); + + final List routes = [ + GoRoute( + path: '/root', + builder: (BuildContext context, GoRouterState state) => + const Text('Root'), + routes: [ + StatefulShellRoute( + builder: (BuildContext context, StatefulShellRouteState state, + Widget child) => + child, + branches: [ + StatefulShellBranch(routes: [ + GoRoute( + path: 'a', + builder: (BuildContext context, GoRouterState state) => + const Text('Screen A'), + ), + ]), + StatefulShellBranch(routes: [ + GoRoute( + path: 'b', + builder: (BuildContext context, GoRouterState state) => + const Text('Screen B'), + ), + ]), + ], + ), + ], + ), + ]; + + final GoRouter router = await createRouter(routes, tester, + initialLocation: '/root/a', navigatorKey: rootNavigatorKey); + expect(find.text('Screen A'), findsOneWidget); + expect(find.text('Screen B'), findsNothing); + + router.go('/root/b'); + await tester.pumpAndSettle(); + expect(find.text('Screen A'), findsNothing); + expect(find.text('Screen B'), findsOneWidget); + }); + testWidgets( 'Navigation with goBranch is correctly handled in StatefulShellRoute', (WidgetTester tester) async { @@ -3986,6 +4072,417 @@ void main() { }, ); }); + + group('state restoration', () { + testWidgets('Restores state correctly', (WidgetTester tester) async { + final GlobalKey statefulWidgetKeyA = + GlobalKey(); + + final List routes = [ + GoRoute( + path: '/a', + pageBuilder: createPageBuilder( + restorationId: 'screenA', child: const Text('Screen A')), + routes: [ + GoRoute( + path: 'detail', + pageBuilder: createPageBuilder( + restorationId: 'screenADetail', + child: Column(children: [ + const Text('Screen A Detail'), + DummyRestorableStatefulWidget( + key: statefulWidgetKeyA, restorationId: 'counterA'), + ])), + ), + ], + ), + ]; + + await createRouter(routes, tester, + initialLocation: '/a/detail', restorationScopeId: 'test'); + await tester.pumpAndSettle(); + statefulWidgetKeyA.currentState?.increment(); + expect(statefulWidgetKeyA.currentState?.counter, equals(1)); + await tester.pumpAndSettle(); // Give state change time to persist + + await tester.restartAndRestore(); + + await tester.pumpAndSettle(); + expect(find.text('Screen A Detail'), findsOneWidget); + expect(statefulWidgetKeyA.currentState?.counter, equals(1)); + }); + + testWidgets('Restores state for imperative routes correctly', + (WidgetTester tester) async { + final GlobalKey statefulWidgetKeyA = + GlobalKey(); + final GlobalKey + statefulWidgetKeyPushed = + GlobalKey(); + + final List routes = [ + GoRoute( + path: '/a', + pageBuilder: createPageBuilder( + restorationId: 'screenA', child: const Text('Screen A')), + routes: [ + GoRoute( + path: 'detail', + pageBuilder: createPageBuilder( + restorationId: 'screenADetail', + child: Column(children: [ + const Text('Screen A Detail'), + DummyRestorableStatefulWidget( + key: statefulWidgetKeyA, restorationId: 'counterA'), + ])), + ), + ], + ), + GoRoute( + path: '/pushed', + pageBuilder: createPageBuilder( + restorationId: 'pushed', + child: Column(children: [ + const Text('Pushed screen'), + DummyRestorableStatefulWidget( + key: statefulWidgetKeyPushed, + restorationId: 'counterPushed'), + ])), + ), + ]; + + final GoRouter router = await createRouter(routes, tester, + initialLocation: '/a/detail', restorationScopeId: 'test'); + await tester.pumpAndSettle(); + statefulWidgetKeyA.currentState?.increment(); + expect(statefulWidgetKeyA.currentState?.counter, equals(1)); + + router.push('/pushed'); + await tester.pumpAndSettle(); + expect(find.text('Pushed screen'), findsOneWidget); + expect(find.text('Screen A Detail'), findsNothing); + statefulWidgetKeyPushed.currentState?.increment(2); + expect(statefulWidgetKeyPushed.currentState?.counter, equals(2)); + await tester.pumpAndSettle(); // Give state change time to persist + + await tester.restartAndRestore(); + + await tester.pumpAndSettle(); + expect(find.text('Pushed screen'), findsOneWidget); + expect(find.text('Screen A Detail'), findsNothing); + expect(statefulWidgetKeyPushed.currentState?.counter, equals(2)); + // Verify that the page key is restored correctly + expect(router.routerDelegate.matches.last.pageKey.value, + equals('/pushed-p1')); + + router.pop(); + await tester.pumpAndSettle(); + expect(find.text('Pushed screen'), findsNothing); + expect(find.text('Screen A Detail'), findsOneWidget); + expect(statefulWidgetKeyA.currentState?.counter, equals(1)); + + router.push('/pushed'); + await tester.pumpAndSettle(); + expect(find.text('Pushed screen'), findsOneWidget); + expect(find.text('Screen A Detail'), findsNothing); + // Verify that the page key is incremented correctly after restore (i.e. + // not starting at 0) + expect(router.routerDelegate.matches.last.pageKey.value, + equals('/pushed-p2')); + }); + + testWidgets('Restores state of branches in StatefulShellRoute correctly', + (WidgetTester tester) async { + final GlobalKey rootNavigatorKey = + GlobalKey(); + final GlobalKey statefulWidgetKeyA = + GlobalKey(); + final GlobalKey statefulWidgetKeyB = + GlobalKey(); + final GlobalKey statefulWidgetKeyC = + GlobalKey(); + StatefulShellRouteState? routeState; + + final List routes = [ + StatefulShellRoute( + restorationScopeId: 'shell', + pageBuilder: (BuildContext context, StatefulShellRouteState state, + Widget child) { + routeState = state; + return MaterialPage( + restorationId: 'shellWidget', child: child); + }, + branches: [ + StatefulShellBranch( + restorationScopeId: 'branchA', + routes: [ + GoRoute( + path: '/a', + pageBuilder: createPageBuilder( + restorationId: 'screenA', + child: const Text('Screen A')), + routes: [ + GoRoute( + path: 'detailA', + pageBuilder: createPageBuilder( + restorationId: 'screenADetail', + child: Column(children: [ + const Text('Screen A Detail'), + DummyRestorableStatefulWidget( + key: statefulWidgetKeyA, + restorationId: 'counterA'), + ])), + ), + ], + ), + ]), + StatefulShellBranch( + restorationScopeId: 'branchB', + routes: [ + GoRoute( + path: '/b', + pageBuilder: createPageBuilder( + restorationId: 'screenB', + child: const Text('Screen B')), + routes: [ + GoRoute( + path: 'detailB', + pageBuilder: createPageBuilder( + restorationId: 'screenBDetail', + child: Column(children: [ + const Text('Screen B Detail'), + DummyRestorableStatefulWidget( + key: statefulWidgetKeyB, + restorationId: 'counterB'), + ])), + ), + ], + ), + ]), + StatefulShellBranch(routes: [ + GoRoute( + path: '/c', + pageBuilder: createPageBuilder( + restorationId: 'screenC', child: const Text('Screen C')), + routes: [ + GoRoute( + path: 'detailC', + pageBuilder: createPageBuilder( + restorationId: 'screenCDetail', + child: Column(children: [ + const Text('Screen C Detail'), + DummyRestorableStatefulWidget( + key: statefulWidgetKeyC, + restorationId: 'counterC'), + ])), + ), + ], + ), + ]), + ], + ), + ]; + + final GoRouter router = await createRouter(routes, tester, + initialLocation: '/a/detailA', + navigatorKey: rootNavigatorKey, + restorationScopeId: 'test'); + await tester.pumpAndSettle(); + statefulWidgetKeyA.currentState?.increment(); + expect(statefulWidgetKeyA.currentState?.counter, equals(1)); + + router.go('/b/detailB'); + await tester.pumpAndSettle(); + statefulWidgetKeyB.currentState?.increment(); + expect(statefulWidgetKeyB.currentState?.counter, equals(1)); + + router.go('/c/detailC'); + await tester.pumpAndSettle(); + statefulWidgetKeyC.currentState?.increment(); + expect(statefulWidgetKeyC.currentState?.counter, equals(1)); + + routeState!.goBranch(index: 0); + await tester.pumpAndSettle(); + expect(find.text('Screen A Detail'), findsOneWidget); + + await tester.restartAndRestore(); + + await tester.pumpAndSettle(); + expect(find.text('Screen A Detail'), findsOneWidget); + expect(statefulWidgetKeyA.currentState?.counter, equals(1)); + + routeState!.goBranch(index: 1); + await tester.pumpAndSettle(); + expect(find.text('Screen B Detail'), findsOneWidget); + expect(statefulWidgetKeyB.currentState?.counter, equals(1)); + + routeState!.goBranch(index: 2); + await tester.pumpAndSettle(); + expect(find.text('Screen C Detail'), findsOneWidget); + // State of branch C should not have been restored + expect(statefulWidgetKeyC.currentState?.counter, equals(0)); + }); + + testWidgets( + 'Restores state of imperative routes in StatefulShellRoute correctly', + (WidgetTester tester) async { + final GlobalKey rootNavigatorKey = + GlobalKey(); + final GlobalKey statefulWidgetKeyA = + GlobalKey(); + final GlobalKey statefulWidgetKeyB = + GlobalKey(); + StatefulShellRouteState? routeStateRoot; + StatefulShellRouteState? routeStateNested; + + final List routes = [ + StatefulShellRoute( + restorationScopeId: 'shell', + pageBuilder: (BuildContext context, StatefulShellRouteState state, + Widget child) { + routeStateRoot = state; + return MaterialPage( + restorationId: 'shellWidget', child: child); + }, + branches: [ + StatefulShellBranch( + restorationScopeId: 'branchA', + routes: [ + GoRoute( + path: '/a', + pageBuilder: createPageBuilder( + restorationId: 'screenA', + child: const Text('Screen A')), + routes: [ + GoRoute( + path: 'detailA', + pageBuilder: createPageBuilder( + restorationId: 'screenADetail', + child: Column(children: [ + const Text('Screen A Detail'), + DummyRestorableStatefulWidget( + key: statefulWidgetKeyA, + restorationId: 'counterA'), + ])), + ), + ], + ), + ]), + StatefulShellBranch( + restorationScopeId: 'branchB', + routes: [ + StatefulShellRoute( + restorationScopeId: 'branchB-nested-shell', + pageBuilder: (BuildContext context, + StatefulShellRouteState state, Widget child) { + routeStateNested = state; + return MaterialPage( + restorationId: 'shellWidget-nested', child: child); + }, + branches: [ + StatefulShellBranch( + restorationScopeId: 'branchB-nested', + routes: [ + GoRoute( + path: '/b', + pageBuilder: createPageBuilder( + restorationId: 'screenB', + child: const Text('Screen B')), + routes: [ + GoRoute( + path: 'detailB', + pageBuilder: createPageBuilder( + restorationId: 'screenBDetail', + child: Column(children: [ + const Text('Screen B Detail'), + DummyRestorableStatefulWidget( + key: statefulWidgetKeyB, + restorationId: 'counterB'), + ])), + ), + ], + ), + // GoRoute( + // path: '/bPushed', + // pageBuilder: createPageBuilder( + // restorationId: 'screenBDetail', + // child: Column(children: [ + // const Text('Screen B Pushed Detail'), + // DummyRestorableStatefulWidget( + // key: statefulWidgetKeyB, + // restorationId: 'counterB'), + // ])), + // ), + ]), + StatefulShellBranch( + restorationScopeId: 'branchC-nested', + routes: [ + GoRoute( + path: '/c', + pageBuilder: createPageBuilder( + restorationId: 'screenC', + child: const Text('Screen C')), + ), + ]), + ]) + ]), + ], + ), + ]; + + final GoRouter router = await createRouter(routes, tester, + initialLocation: '/a/detailA', + navigatorKey: rootNavigatorKey, + restorationScopeId: 'test'); + await tester.pumpAndSettle(); + statefulWidgetKeyA.currentState?.increment(); + expect(statefulWidgetKeyA.currentState?.counter, equals(1)); + + routeStateRoot!.goBranch(index: 1); + await tester.pumpAndSettle(); + + // router.push('/bPushed'); + router.go('/b/detailB'); + await tester.pumpAndSettle(); + statefulWidgetKeyB.currentState?.increment(); + expect(statefulWidgetKeyB.currentState?.counter, equals(1)); + + routeStateRoot!.goBranch(index: 0); + await tester.pumpAndSettle(); + expect(find.text('Screen A Detail'), findsOneWidget); + expect(find.text('Screen B'), findsNothing); + expect(find.text('Screen B Pushed Detail'), findsNothing); + + await tester.restartAndRestore(); + + await tester.pumpAndSettle(); + expect(find.text('Screen A Detail'), findsOneWidget); + expect(find.text('Screen B'), findsNothing); + expect(find.text('Screen B Pushed Detail'), findsNothing); + expect(statefulWidgetKeyA.currentState?.counter, equals(1)); + + routeStateRoot!.goBranch(index: 1); + await tester.pumpAndSettle(); + expect(find.text('Screen A Detail'), findsNothing); + expect(find.text('Screen B'), findsNothing); + //expect(find.text('Screen B Pushed Detail'), findsOneWidget); + expect(find.text('Screen B Detail'), findsOneWidget); + expect(statefulWidgetKeyB.currentState?.counter, equals(1)); + + routeStateNested!.goBranch(index: 1); + await tester.pumpAndSettle(); + routeStateNested!.goBranch(index: 0); + await tester.pumpAndSettle(); + + expect(find.text('Screen B Detail'), findsOneWidget); + expect(statefulWidgetKeyB.currentState?.counter, equals(1)); + + // router.pop(); + // await tester.pumpAndSettle(); + // expect(find.text('Screen B'), findsOneWidget); + // expect(find.text('Screen B Pushed Detail'), findsNothing); + }); + }); } /// This allows a value of type T or T? to be treated as a value of type T?. diff --git a/packages/go_router/test/match_test.dart b/packages/go_router/test/match_test.dart index 73531227b046..22937c7849fb 100644 --- a/packages/go_router/test/match_test.dart +++ b/packages/go_router/test/match_test.dart @@ -160,10 +160,9 @@ void main() { const {}, ); - final UnmodifiableRouteMatchList list1 = list.unmodifiableMatchList(); - final UnmodifiableRouteMatchList list2 = list.unmodifiableMatchList(); + final RouteMatchList list2 = list.clone(); - expect(list1, equals(list2)); + expect(RouteMatchList.matchListEquals(list, list2), true); }); }); } diff --git a/packages/go_router/test/matching_test.dart b/packages/go_router/test/matching_test.dart index c92533bf3813..f4bc74ca07b5 100644 --- a/packages/go_router/test/matching_test.dart +++ b/packages/go_router/test/matching_test.dart @@ -5,6 +5,7 @@ import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:go_router/src/configuration.dart'; +import 'package:go_router/src/delegate.dart'; import 'package:go_router/src/matching.dart'; import 'package:go_router/src/router.dart'; @@ -27,4 +28,37 @@ void main() { final RouteMatchList matches = router.routerDelegate.matches; expect(matches.toString(), contains('/page-0')); }); + + test('RouteMatchList is encoded and decoded correctly', () { + final RouteConfiguration configuration = RouteConfiguration( + routes: [ + GoRoute( + path: '/a', + builder: (BuildContext context, GoRouterState state) => + const Placeholder(), + ), + GoRoute( + path: '/b', + builder: (BuildContext context, GoRouterState state) => + const Placeholder(), + ), + ], + redirectLimit: 0, + navigatorKey: GlobalKey(), + topRedirect: (_, __) => null, + ); + final RouteMatcher matcher = RouteMatcher(configuration); + final RouteMatchListCodec codec = RouteMatchListCodec(matcher); + + final RouteMatchList list1 = matcher.findMatch('/a'); + final RouteMatchList list2 = matcher.findMatch('/b'); + list1.push(ImperativeRouteMatch( + pageKey: const ValueKey('/b-p0'), matches: list2)); + + final Object? encoded = codec.encodeMatchList(list1); + final RouteMatchList? decoded = codec.decodeMatchList(encoded); + + expect(decoded, isNotNull); + expect(RouteMatchList.matchListEquals(decoded!, list1), isTrue); + }); } diff --git a/packages/go_router/test/test_helpers.dart b/packages/go_router/test/test_helpers.dart index 1000e60b56c5..290d09605761 100644 --- a/packages/go_router/test/test_helpers.dart +++ b/packages/go_router/test/test_helpers.dart @@ -8,6 +8,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:go_router/go_router.dart'; +import 'package:go_router/src/match.dart'; Future createGoRouter(WidgetTester tester) async { final GoRouter goRouter = GoRouter( @@ -144,19 +145,22 @@ Future createRouter( int redirectLimit = 5, GlobalKey? navigatorKey, GoRouterWidgetBuilder? errorBuilder, + String? restorationScopeId, }) async { final GoRouter goRouter = GoRouter( - routes: routes, - redirect: redirect, - initialLocation: initialLocation, - redirectLimit: redirectLimit, - errorBuilder: errorBuilder ?? - (BuildContext context, GoRouterState state) => - TestErrorScreen(state.error!), - navigatorKey: navigatorKey, - ); + routes: routes, + redirect: redirect, + initialLocation: initialLocation, + redirectLimit: redirectLimit, + errorBuilder: errorBuilder ?? + (BuildContext context, GoRouterState state) => + TestErrorScreen(state.error!), + navigatorKey: navigatorKey, + restorationScopeId: restorationScopeId); await tester.pumpWidget( MaterialApp.router( + restorationScopeId: + restorationScopeId != null ? '$restorationScopeId-root' : null, routerConfig: goRouter, ), ); @@ -224,7 +228,7 @@ class DummyStatefulWidget extends StatefulWidget { const DummyStatefulWidget({super.key}); @override - State createState() => DummyStatefulWidgetState(); + State createState() => DummyStatefulWidgetState(); } class DummyStatefulWidgetState extends State { @@ -238,6 +242,39 @@ class DummyStatefulWidgetState extends State { Widget build(BuildContext context) => Container(); } +class DummyRestorableStatefulWidget extends StatefulWidget { + const DummyRestorableStatefulWidget({super.key, this.restorationId}); + + final String? restorationId; + + @override + State createState() => DummyRestorableStatefulWidgetState(); +} + +class DummyRestorableStatefulWidgetState + extends State with RestorationMixin { + final RestorableInt _counter = RestorableInt(0); + + @override + String? get restorationId => widget.restorationId; + + int get counter => _counter.value; + + void increment([int count = 1]) => setState(() { + _counter.value += count; + }); + + @override + void restoreState(RestorationBucket? oldBucket, bool initialRestore) { + if (restorationId != null) { + registerForRestoration(_counter, restorationId!); + } + } + + @override + Widget build(BuildContext context) => Container(); +} + Future simulateAndroidBackButton(WidgetTester tester) async { final ByteData message = const JSONMethodCodec().encodeMethodCall(const MethodCall('popRoute')); @@ -245,7 +282,22 @@ Future simulateAndroidBackButton(WidgetTester tester) async { .handlePlatformMessage('flutter/navigation', message, (_) {}); } +GoRouterPageBuilder createPageBuilder( + {String? restorationId, required Widget child}) => + (BuildContext context, GoRouterState state) => + MaterialPage(restorationId: restorationId, child: child); + StatefulShellRouteBuilder mockStatefulShellBuilder = (BuildContext context, StatefulShellRouteState state, Widget child) { return child; }; + +RouteMatch createRouteMatch(RouteBase route, String location) { + return RouteMatch( + route: route, + subloc: location, + extra: null, + error: null, + pageKey: ValueKey(location), + ); +} From 9983887e15dbb50cc5d63fc772e0a8972ab2fea4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20Lo=CC=88fstrand?= Date: Thu, 6 Apr 2023 18:58:46 +0200 Subject: [PATCH 094/112] Renamed StatefulShellRoute to StackedShellRoute --- packages/go_router/CHANGELOG.md | 4 +- packages/go_router/example/README.md | 6 +- ...t => stacked_shell_state_restoration.dart} | 42 +-- ...ll_route.dart => stacked_shell_route.dart} | 50 +-- packages/go_router/lib/go_router.dart | 12 +- packages/go_router/lib/src/builder.dart | 13 +- packages/go_router/lib/src/configuration.dart | 38 +-- packages/go_router/lib/src/matching.dart | 3 +- packages/go_router/lib/src/route.dart | 242 +++++++------ packages/go_router/lib/src/typedefs.dart | 12 +- packages/go_router/test/builder_test.dart | 10 +- .../go_router/test/configuration_test.dart | 124 +++---- packages/go_router/test/delegate_test.dart | 22 +- packages/go_router/test/go_router_test.dart | 317 +++++++++--------- packages/go_router/test/test_helpers.dart | 4 +- 15 files changed, 444 insertions(+), 455 deletions(-) rename packages/go_router/example/lib/others/{stateful_shell_state_restoration.dart => stacked_shell_state_restoration.dart} (92%) rename packages/go_router/example/lib/{stateful_shell_route.dart => stacked_shell_route.dart} (92%) diff --git a/packages/go_router/CHANGELOG.md b/packages/go_router/CHANGELOG.md index 41f7ae07d2e5..ec3eba01538f 100644 --- a/packages/go_router/CHANGELOG.md +++ b/packages/go_router/CHANGELOG.md @@ -1,11 +1,11 @@ ## 6.6.0 -- Introduces `StatefulShellRoute` to support using separate +- Introduces `StackedShellRoute` to support using separate navigators for child routes as well as preserving state in each navigation tree (flutter/flutter#99124). - Updates documentation for `pageBuilder` and `builder` fields of `ShellRoute`, to more correctly describe the meaning of the child argument in the builder functions. -- Adds support for restorationId to ShellRoute (and StatefulShellRoute). +- Adds support for restorationId to ShellRoute (and StackedShellRoute). - Adds support for restoring imperatively pushed routes. ## 6.5.2 diff --git a/packages/go_router/example/README.md b/packages/go_router/example/README.md index 5ee817ac7b41..eeb9d252ce52 100644 --- a/packages/go_router/example/README.md +++ b/packages/go_router/example/README.md @@ -30,10 +30,10 @@ An example to demonstrate how to use redirect to handle a synchronous sign-in fl An example to demonstrate how to use handle a sign-in flow with a stream authentication service. -## [Stateful Nested Navigation](https://github.com/flutter/packages/blob/main/packages/go_router/example/lib/stateful_shell_route.dart) -`flutter run lib/stateful_shell_route.dart` +## [Stateful Nested Navigation](https://github.com/flutter/packages/blob/main/packages/go_router/example/lib/stacked_shell_route.dart) +`flutter run lib/stacked_shell_route.dart` -An example to demonstrate how to use a `StatefulShellRoute` to create stateful nested navigation, with a +An example to demonstrate how to use a `StackedShellRoute` to create stateful nested navigation, with a `BottomNavigationBar`. ## [Books app](https://github.com/flutter/packages/blob/main/packages/go_router/example/lib/books) diff --git a/packages/go_router/example/lib/others/stateful_shell_state_restoration.dart b/packages/go_router/example/lib/others/stacked_shell_state_restoration.dart similarity index 92% rename from packages/go_router/example/lib/others/stateful_shell_state_restoration.dart rename to packages/go_router/example/lib/others/stacked_shell_state_restoration.dart index 5dbea3632bf3..f43024011433 100644 --- a/packages/go_router/example/lib/others/stateful_shell_state_restoration.dart +++ b/packages/go_router/example/lib/others/stacked_shell_state_restoration.dart @@ -11,24 +11,24 @@ final GlobalKey _rootNavigatorKey = final GlobalKey _tabANavigatorKey = GlobalKey(debugLabel: 'tabANav'); -void main() => runApp(RestorableStatefulShellRouteExampleApp()); +void main() => runApp(RestorableStackedShellRouteExampleApp()); -/// An example demonstrating how to use StatefulShellRoute with state +/// An example demonstrating how to use StackedShellRoute with state /// restoration. -class RestorableStatefulShellRouteExampleApp extends StatelessWidget { +class RestorableStackedShellRouteExampleApp extends StatelessWidget { /// Creates a NestedTabNavigationExampleApp - RestorableStatefulShellRouteExampleApp({super.key}); + RestorableStackedShellRouteExampleApp({super.key}); final GoRouter _router = GoRouter( navigatorKey: _rootNavigatorKey, initialLocation: '/a', restorationScopeId: 'router', routes: [ - StatefulShellRoute( + StackedShellRoute( restorationScopeId: 'shell1', - branches: [ + branches: [ /// The route branch for the first tab of the bottom navigation bar. - StatefulShellBranch( + StackedShellBranch( navigatorKey: _tabANavigatorKey, restorationScopeId: 'branchA', routes: [ @@ -59,16 +59,16 @@ class RestorableStatefulShellRouteExampleApp extends StatelessWidget { ), /// The route branch for the third tab of the bottom navigation bar. - StatefulShellBranch( + StackedShellBranch( restorationScopeId: 'branchB', routes: [ - StatefulShellRoute( + StackedShellRoute( restorationScopeId: 'shell2', /// This bottom tab uses a nested shell, wrapping sub routes in a /// top TabBar. - branches: [ - StatefulShellBranch( + branches: [ + StackedShellBranch( restorationScopeId: 'branchB1', routes: [ GoRoute( @@ -96,7 +96,7 @@ class RestorableStatefulShellRouteExampleApp extends StatelessWidget { ], ), ]), - StatefulShellBranch( + StackedShellBranch( restorationScopeId: 'branchB2', routes: [ GoRoute( @@ -126,13 +126,13 @@ class RestorableStatefulShellRouteExampleApp extends StatelessWidget { ]), ], pageBuilder: (BuildContext context, - StatefulShellRouteState state, Widget child) { + StackedShellRouteState state, Widget child) { return MaterialPage( restorationId: 'shellWidget2', - child: StatefulNavigationShell( + child: StackedNavigationShell( shellRouteState: state, containerBuilder: (BuildContext context, - StatefulShellRouteState state, + StackedShellRouteState state, List children) => TabbedRootScreen( shellState: state, children: children), @@ -142,8 +142,8 @@ class RestorableStatefulShellRouteExampleApp extends StatelessWidget { ], ), ], - pageBuilder: (BuildContext context, StatefulShellRouteState state, - Widget child) { + pageBuilder: + (BuildContext context, StackedShellRouteState state, Widget child) { return MaterialPage( restorationId: 'shellWidget1', child: ScaffoldWithNavBar(shellState: state, body: child)); @@ -175,8 +175,8 @@ class ScaffoldWithNavBar extends StatelessWidget { Key? key, }) : super(key: key ?? const ValueKey('ScaffoldWithNavBar')); - /// The current state of the parent StatefulShellRoute. - final StatefulShellRouteState shellState; + /// The current state of the parent StackedShellRoute. + final StackedShellRouteState shellState; /// Body, i.e. the container for the branch Navigators. final Widget body; @@ -346,8 +346,8 @@ class TabbedRootScreen extends StatefulWidget { const TabbedRootScreen( {required this.shellState, required this.children, super.key}); - /// The current state of the parent StatefulShellRoute. - final StatefulShellRouteState shellState; + /// The current state of the parent StackedShellRoute. + final StackedShellRouteState shellState; /// The children (Navigators) to display in the [TabBarView]. final List children; diff --git a/packages/go_router/example/lib/stateful_shell_route.dart b/packages/go_router/example/lib/stacked_shell_route.dart similarity index 92% rename from packages/go_router/example/lib/stateful_shell_route.dart rename to packages/go_router/example/lib/stacked_shell_route.dart index 463b4b778ca8..5883d5872a8d 100644 --- a/packages/go_router/example/lib/stateful_shell_route.dart +++ b/packages/go_router/example/lib/stacked_shell_route.dart @@ -16,7 +16,7 @@ final GlobalKey _tabANavigatorKey = // navigation state is maintained separately for each tab. This setup also // enables deep linking into nested pages. // -// This example demonstrates how to display routes within a StatefulShellRoute, +// This example demonstrates how to display routes within a StackedShellRoute, // that are places on separate navigators. The example also demonstrates how // state is maintained when switching between different tabs (and thus branches // and Navigators). @@ -40,10 +40,10 @@ class NestedTabNavigationExampleApp extends StatelessWidget { builder: (BuildContext context, GoRouterState state) => const ModalScreen(), ), - StatefulShellRoute( - branches: [ + StackedShellRoute( + branches: [ /// The route branch for the first tab of the bottom navigation bar. - StatefulShellBranch( + StackedShellBranch( navigatorKey: _tabANavigatorKey, routes: [ GoRoute( @@ -67,7 +67,7 @@ class NestedTabNavigationExampleApp extends StatelessWidget { ), /// The route branch for the second tab of the bottom navigation bar. - StatefulShellBranch( + StackedShellBranch( /// It's not necessary to provide a navigatorKey if it isn't also /// needed elsewhere. If not provided, a default key will be used. // navigatorKey: _tabBNavigatorKey, @@ -98,18 +98,18 @@ class NestedTabNavigationExampleApp extends StatelessWidget { ), /// The route branch for the third tab of the bottom navigation bar. - StatefulShellBranch( - /// StatefulShellBranch will automatically use the first descendant + StackedShellBranch( + /// StackedShellBranch 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', routes: [ - StatefulShellRoute( + StackedShellRoute( /// This bottom tab uses a nested shell, wrapping sub routes in a /// top TabBar. - branches: [ - StatefulShellBranch(routes: [ + branches: [ + StackedShellBranch(routes: [ GoRoute( path: '/c1', builder: (BuildContext context, GoRouterState state) => @@ -129,7 +129,7 @@ class NestedTabNavigationExampleApp extends StatelessWidget { ], ), ]), - StatefulShellBranch(routes: [ + StackedShellBranch(routes: [ GoRoute( path: '/c2', builder: (BuildContext context, GoRouterState state) => @@ -150,21 +150,21 @@ class NestedTabNavigationExampleApp extends StatelessWidget { ), ]), ], - builder: (BuildContext context, StatefulShellRouteState state, + builder: (BuildContext context, StackedShellRouteState state, Widget child) { - /// This nested StatefulShellRoute demonstrates the use of a + /// This nested StackedShellRoute demonstrates the use of a /// custom container (TabBarView) for the branch Navigators. /// Here, the default branch Navigator container (`child`) is - /// ignored, and the class StatefulNavigationShell is + /// ignored, and the class StackedNavigationShell is /// instead used to provide access to the widgets representing /// the branch Navigators (`List children`). /// /// See TabbedRootScreen for more details on how the children /// are used in the TabBarView. - return StatefulNavigationShell( + return StackedNavigationShell( shellRouteState: state, containerBuilder: (BuildContext context, - StatefulShellRouteState state, + StackedShellRouteState state, List children) => TabbedRootScreen(shellState: state, children: children), ); @@ -173,18 +173,18 @@ class NestedTabNavigationExampleApp extends StatelessWidget { ], ), ], - builder: (BuildContext context, StatefulShellRouteState state, - Widget child) { + builder: + (BuildContext context, StackedShellRouteState state, Widget child) { /// This builder implementation uses the default container for the /// branch Navigators (provided in through the `child` argument). This - /// is the simplest way to use StatefulShellRoute, where the shell is + /// is the simplest way to use StackedShellRoute, where the shell is /// built around the Navigator container (see ScaffoldWithNavBar). return ScaffoldWithNavBar(shellState: state, body: child); }, - /// If it's necessary to customize the Page for StatefulShellRoute, + /// If it's necessary to customize the Page for StackedShellRoute, /// provide a pageBuilder function instead of the builder, for example: - // pageBuilder: (BuildContext context, StatefulShellRouteState state, + // pageBuilder: (BuildContext context, StackedShellRouteState state, // Widget child) { // return NoTransitionPage( // child: ScaffoldWithNavBar(shellState: state, body: child)); @@ -215,8 +215,8 @@ class ScaffoldWithNavBar extends StatelessWidget { Key? key, }) : super(key: key ?? const ValueKey('ScaffoldWithNavBar')); - /// The current state of the parent StatefulShellRoute. - final StatefulShellRouteState shellState; + /// The current state of the parent StackedShellRoute. + final StackedShellRouteState shellState; /// Body, i.e. the container for the branch Navigators. final Widget body; @@ -461,8 +461,8 @@ class TabbedRootScreen extends StatefulWidget { const TabbedRootScreen( {required this.shellState, required this.children, super.key}); - /// The current state of the parent StatefulShellRoute. - final StatefulShellRouteState shellState; + /// The current state of the parent StackedShellRoute. + final StackedShellRouteState shellState; /// The children (Navigators) to display in the [TabBarView]. final List children; diff --git a/packages/go_router/lib/go_router.dart b/packages/go_router/lib/go_router.dart index dfe9d979c4cb..5d377f427715 100644 --- a/packages/go_router/lib/go_router.dart +++ b/packages/go_router/lib/go_router.dart @@ -13,10 +13,10 @@ export 'src/configuration.dart' RouteBase, ShellRoute, ShellNavigatorContainerBuilder, - StatefulNavigationShell, - StatefulShellBranch, - StatefulShellRoute, - StatefulShellRouteState; + StackedNavigationShell, + StackedShellBranch, + StackedShellRoute, + StackedShellRouteState; export 'src/misc/extensions.dart'; export 'src/misc/inherited_router.dart'; export 'src/pages/custom_transition_page.dart'; @@ -36,5 +36,5 @@ export 'src/typedefs.dart' GoRouterWidgetBuilder, ShellRouteBuilder, ShellRoutePageBuilder, - StatefulShellRouteBuilder, - StatefulShellRoutePageBuilder; + StackedShellRouteBuilder, + StackedShellRoutePageBuilder; diff --git a/packages/go_router/lib/src/builder.dart b/packages/go_router/lib/src/builder.dart index 253979b11fe1..f14a12b9533a 100644 --- a/packages/go_router/lib/src/builder.dart +++ b/packages/go_router/lib/src/builder.dart @@ -153,10 +153,9 @@ class RouteBuilder { } finally { /// Clean up previous cache to prevent memory leak, making sure any nested /// stateful shell routes for the current match list are kept. - final Iterable matchListShellRoutes = matchList - .matches + final Iterable matchListShellRoutes = matchList.matches .map((RouteMatch e) => e.route) - .whereType(); + .whereType(); final Set activeKeys = keyToPage.keys.toSet() ..addAll(_nestedStatefulNavigatorKeys(matchListShellRoutes)); @@ -166,11 +165,11 @@ class RouteBuilder { } Set> _nestedStatefulNavigatorKeys( - Iterable routes) { + Iterable routes) { return RouteConfiguration.routesRecursively(routes) - .whereType() - .expand((StatefulShellRoute e) => - e.branches.map((StatefulShellBranch b) => b.navigatorKey)) + .whereType() + .expand((StackedShellRoute e) => + e.branches.map((StackedShellBranch b) => b.navigatorKey)) .toSet(); } diff --git a/packages/go_router/lib/src/configuration.dart b/packages/go_router/lib/src/configuration.dart index e1f1b3046ed5..806356a4c6e0 100644 --- a/packages/go_router/lib/src/configuration.dart +++ b/packages/go_router/lib/src/configuration.dart @@ -27,7 +27,7 @@ class RouteConfiguration { _debugVerifyNoDuplicatePathParameter(routes, {})), assert(_debugCheckParentNavigatorKeys( routes, >[navigatorKey])) { - assert(_debugCheckStatefulShellBranchDefaultLocations( + assert(_debugCheckStackedShellBranchDefaultLocations( routes, RouteMatcher(this))); _cacheNameToPath('', routes); log.info(_debugKnownRoutes()); @@ -90,11 +90,11 @@ class RouteConfiguration { route.routes, >[...allowedKeys..add(route.navigatorKey)], ); - } else if (route is StatefulShellRoute) { - for (final StatefulShellBranch branch in route.branches) { + } else if (route is StackedShellRoute) { + for (final StackedShellBranch branch in route.branches) { assert( !allowedKeys.contains(branch.navigatorKey), - 'StatefulShellBranch must not reuse an ancestor navigatorKey ' + 'StackedShellBranch must not reuse an ancestor navigatorKey ' '(${branch.navigatorKey})'); _debugCheckParentNavigatorKeys( @@ -130,18 +130,18 @@ class RouteConfiguration { return true; } - // Check to see that the configured initialLocation of StatefulShellBranches + // Check to see that the configured initialLocation of StackedShellBranches // points to a descendant route of the route branch. - bool _debugCheckStatefulShellBranchDefaultLocations( + bool _debugCheckStackedShellBranchDefaultLocations( List routes, RouteMatcher matcher) { try { for (final RouteBase route in routes) { - if (route is StatefulShellRoute) { - for (final StatefulShellBranch branch in route.branches) { + if (route is StackedShellRoute) { + for (final StackedShellBranch branch in route.branches) { if (branch.initialLocation == null) { // Recursively search for the first GoRoute descendant. Will // throw assertion error if not found. - findStatefulShellBranchDefaultLocation(branch); + findStackedShellBranchDefaultLocation(branch); } else { final RouteBase initialLocationRoute = matcher.findMatch(branch.initialLocation!).last.route; @@ -151,17 +151,17 @@ class RouteConfiguration { assert( match != null, 'The initialLocation (${branch.initialLocation}) of ' - 'StatefulShellBranch must match a descendant route of the ' + 'StackedShellBranch must match a descendant route of the ' 'branch'); } } } - _debugCheckStatefulShellBranchDefaultLocations(route.routes, matcher); + _debugCheckStackedShellBranchDefaultLocations(route.routes, matcher); } } on MatcherError catch (e) { assert( false, - 'initialLocation (${e.location}) of StatefulShellBranch must ' + 'initialLocation (${e.location}) of StackedShellBranch must ' 'be a valid location'); } return true; @@ -185,30 +185,30 @@ class RouteConfiguration { {required RouteBase ancestor, required RouteBase route}) => ancestor == route || routesRecursively(ancestor.routes).contains(route); - /// Recursively traverses the routes of the provided StatefulShellBranch to + /// Recursively traverses the routes of the provided StackedShellBranch to /// find the first GoRoute, from which a full path will be derived. - String findStatefulShellBranchDefaultLocation(StatefulShellBranch branch) { + String findStackedShellBranchDefaultLocation(StackedShellBranch branch) { final GoRoute? route = _findFirstGoRoute(branch.routes); final String? initialLocation = route != null ? _fullPathForRoute(route, '', routes) : null; assert( initialLocation != null, - 'The initial location of a StatefulShellBranch must be derivable from ' + 'The initial location of a StackedShellBranch must be derivable from ' 'GoRoute descendant'); return initialLocation!; } - /// Returns the effective initial location of a StatefulShellBranch. + /// Returns the effective initial location of a StackedShellBranch. /// /// If the initial location of the branch is null, - /// [findStatefulShellBranchDefaultLocation] is used to calculate the initial + /// [findStackedShellBranchDefaultLocation] is used to calculate the initial /// location. - String effectiveInitialBranchLocation(StatefulShellBranch branch) { + String effectiveInitialBranchLocation(StackedShellBranch branch) { final String? initialLocation = branch.initialLocation; if (initialLocation != null) { return initialLocation; } else { - return findStatefulShellBranchDefaultLocation(branch); + return findStackedShellBranchDefaultLocation(branch); } } diff --git a/packages/go_router/lib/src/matching.dart b/packages/go_router/lib/src/matching.dart index 3c5c06ff3de3..44e755a57191 100644 --- a/packages/go_router/lib/src/matching.dart +++ b/packages/go_router/lib/src/matching.dart @@ -292,7 +292,8 @@ class RouteMatchListCodec { final RouteMatchList imperativeMatchList = _matcher.findMatch( match[_locationKey] as String, extra: match[_stateKey]); - final ImperativeRouteMatch imperativeMatch = ImperativeRouteMatch( + final ImperativeRouteMatch imperativeMatch = + ImperativeRouteMatch( pageKey: pageKey, matches: imperativeMatchList, ); diff --git a/packages/go_router/lib/src/route.dart b/packages/go_router/lib/src/route.dart index b55de3cf7a87..f97d8089a888 100644 --- a/packages/go_router/lib/src/route.dart +++ b/packages/go_router/lib/src/route.dart @@ -325,7 +325,7 @@ class GoRoute extends RouteBase { } /// Base class for classes that act as shells for sub-routes, such -/// as [ShellRoute] and [StatefulShellRoute]. +/// as [ShellRoute] and [StackedShellRoute]. abstract class ShellRouteBase extends RouteBase { /// Constructs a [ShellRouteBase]. const ShellRouteBase._({super.routes}) : super._(); @@ -567,23 +567,23 @@ class ShellRoute extends ShellRouteBase { /// implementing a UI with a [BottomNavigationBar], with a persistent navigation /// state for each tab. /// -/// A StatefulShellRoute is created by specifying a List of -/// [StatefulShellBranch] items, each representing a separate stateful branch -/// in the route tree. StatefulShellBranch provides the root routes and the +/// A StackedShellRoute is created by specifying a List of +/// [StackedShellBranch] items, each representing a separate stateful branch +/// in the route tree. StackedShellBranch provides the root routes and the /// Navigator key ([GlobalKey]) for the branch, as well as an optional initial /// location. /// /// 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 [StatefulShellRouteState] parameter instead of a -/// GoRouterState. The StatefulShellRouteState can be used to access information +/// when creating a StackedShellRoute. However, these builders differ slightly +/// in that they accept a [StackedShellRouteState] parameter instead of a +/// GoRouterState. The StackedShellRouteState 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 [StatefulShellRouteState.goBranch], for +/// accomplished by using the method [StackedShellRouteState.goBranch], for /// example: /// /// ``` -/// void _onItemTapped(StatefulShellRouteState shellState, int index) { +/// void _onItemTapped(StackedShellRouteState shellState, int index) { /// shellState.goBranch(index: index); /// } /// ``` @@ -596,13 +596,13 @@ class ShellRoute extends ShellRouteBase { /// Sometimes greater control is needed over the layout and animations of the /// Widgets representing the branch Navigators. In such cases, a custom /// implementation can choose to ignore the child parameter of the builders and -/// instead create a [StatefulNavigationShell], which will manage the state -/// of the StatefulShellRoute. When creating this controller, a builder function +/// instead create a [StackedNavigationShell], which will manage the state +/// of the StackedShellRoute. When creating this controller, a builder function /// is provided to create the container Widget for the branch Navigators. See /// [ShellNavigatorContainerBuilder] for more details. /// /// Below is a simple example of how a router configuration with -/// StatefulShellRoute could be achieved. In this example, a +/// StackedShellRoute could be achieved. In this example, a /// BottomNavigationBar with two tabs is used, and each of the tabs gets its /// own Navigator. A container widget responsible for managing the Navigators /// for all route branches will then be passed as the child argument @@ -617,14 +617,14 @@ class ShellRoute extends ShellRouteBase { /// final GoRouter _router = GoRouter( /// initialLocation: '/a', /// routes: [ -/// StatefulShellRoute( -/// builder: (BuildContext context, StatefulShellRouteState state, +/// StackedShellRoute( +/// builder: (BuildContext context, StackedShellRouteState state, /// Widget child) { /// return ScaffoldWithNavBar(shellState: state, body: child); /// }, /// branches: [ /// /// The first branch, i.e. tab 'A' -/// StatefulShellBranch( +/// StackedShellBranch( /// navigatorKey: _tabANavigatorKey, /// routes: [ /// GoRoute( @@ -643,7 +643,7 @@ class ShellRoute extends ShellRouteBase { /// ], /// ), /// /// The second branch, i.e. tab 'B' -/// StatefulShellBranch( +/// StackedShellBranch( /// navigatorKey: _tabBNavigatorKey, /// routes: [ /// GoRoute( @@ -667,18 +667,18 @@ class ShellRoute extends ShellRouteBase { /// ); /// ``` /// -/// See [Stateful Nested Navigation](https://github.com/flutter/packages/blob/main/packages/go_router/example/lib/stateful_shell_route.dart) -/// for a complete runnable example using StatefulShellRoute. -class StatefulShellRoute extends ShellRouteBase { - /// Constructs a [StatefulShellRoute] from a list of [StatefulShellBranch]es, +/// 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 StackedShellRoute. +class StackedShellRoute extends ShellRouteBase { + /// Constructs a [StackedShellRoute] from a list of [StackedShellBranch]es, /// each representing a separate nested navigation tree (branch). /// /// A separate [Navigator] will be created for each of the branches, using - /// the navigator key specified in [StatefulShellBranch]. Note that unlike + /// the navigator key specified in [StackedShellBranch]. Note that unlike /// [ShellRoute], a builder must always be provided when creating a - /// StatefulShellRoute. The pageBuilder however is optional, and is used + /// StackedShellRoute. The pageBuilder however is optional, and is used /// in addition to the builder. - StatefulShellRoute({ + StackedShellRoute({ required this.branches, this.builder, this.pageBuilder, @@ -704,14 +704,14 @@ class StatefulShellRoute extends ShellRouteBase { /// Widget. /// /// Instead of a GoRouterState, this builder function accepts a - /// [StatefulShellRouteState] object, which can be used to access information + /// [StackedShellRouteState] object, which can be used to access information /// about which branch is active, and also to navigate to a different branch - /// (using [StatefulShellRouteState.goBranch]). + /// (using [StackedShellRouteState.goBranch]). /// /// Custom implementations may choose to ignore the child parameter passed to - /// the builder function, and instead use [StatefulNavigationShell] to + /// the builder function, and instead use [StackedNavigationShell] to /// create a custom container for the branch Navigators. - final StatefulShellRouteBuilder? builder; + final StackedShellRouteBuilder? builder; /// The page builder for a stateful shell route. /// @@ -721,30 +721,30 @@ class StatefulShellRoute extends ShellRouteBase { /// Widget. /// /// Instead of a GoRouterState, this builder function accepts a - /// [StatefulShellRouteState] object, which can be used to access information + /// [StackedShellRouteState] object, which can be used to access information /// about which branch is active, and also to navigate to a different branch - /// (using [StatefulShellRouteState.goBranch]). + /// (using [StackedShellRouteState.goBranch]). /// /// Custom implementations may choose to ignore the child parameter passed to - /// the builder function, and instead use [StatefulNavigationShell] to + /// the builder function, and instead use [StackedNavigationShell] to /// create a custom container for the branch Navigators. - final StatefulShellRoutePageBuilder? pageBuilder; + final StackedShellRoutePageBuilder? pageBuilder; /// Representations of the different stateful route branches that this /// shell route will manage. /// /// Each branch uses a separate [Navigator], identified - /// [StatefulShellBranch.navigatorKey]. - final List branches; + /// [StackedShellBranch.navigatorKey]. + final List branches; - final GlobalKey _shellStateKey = - GlobalKey(); + final GlobalKey _shellStateKey = + GlobalKey(); @override Widget? buildWidget(BuildContext context, GoRouterState state, ShellRouteContext shellRouteContext) { if (builder != null) { - final StatefulNavigationShell shell = + final StackedNavigationShell shell = _createShell(context, state, shellRouteContext); return builder!(context, shell.shellRouteState, shell); } @@ -755,7 +755,7 @@ class StatefulShellRoute extends ShellRouteBase { Page? buildPage(BuildContext context, GoRouterState state, ShellRouteContext shellRouteContext) { if (pageBuilder != null) { - final StatefulNavigationShell shell = + final StackedNavigationShell shell = _createShell(context, state, shellRouteContext); return pageBuilder!(context, shell.shellRouteState, shell); } @@ -764,17 +764,17 @@ class StatefulShellRoute extends ShellRouteBase { @override GlobalKey navigatorKeyForSubRoute(RouteBase subRoute) { - final StatefulShellBranch? branch = branches.firstWhereOrNull( - (StatefulShellBranch e) => e.routes.contains(subRoute)); + final StackedShellBranch? branch = branches.firstWhereOrNull( + (StackedShellBranch e) => e.routes.contains(subRoute)); assert(branch != null); return branch!.navigatorKey; } - StatefulNavigationShell _createShell(BuildContext context, - GoRouterState state, ShellRouteContext shellRouteContext) { + StackedNavigationShell _createShell(BuildContext context, GoRouterState state, + ShellRouteContext shellRouteContext) { final GlobalKey navigatorKey = navigatorKeyForSubRoute(shellRouteContext.subRoute); - final StatefulShellRouteState shellRouteState = StatefulShellRouteState._( + final StackedShellRouteState shellRouteState = StackedShellRouteState._( GoRouter.of(context), this, _shellStateKey, @@ -782,20 +782,20 @@ class StatefulShellRoute extends ShellRouteBase { navigatorKey, shellRouteContext, ); - return StatefulNavigationShell(shellRouteState: shellRouteState); + return StackedNavigationShell(shellRouteState: shellRouteState); } - static List _routes(List branches) => - branches.expand((StatefulShellBranch e) => e.routes).toList(); + static List _routes(List branches) => + branches.expand((StackedShellBranch e) => e.routes).toList(); static Set> _debugUniqueNavigatorKeys( - List branches) => + List branches) => Set>.from( - branches.map((StatefulShellBranch e) => e.navigatorKey)); + branches.map((StackedShellBranch e) => e.navigatorKey)); static bool _debugValidateParentNavigatorKeys( - List branches) { - for (final StatefulShellBranch branch in branches) { + List branches) { + for (final StackedShellBranch branch in branches) { for (final RouteBase route in branch.routes) { if (route is GoRoute) { assert(route.parentNavigatorKey == null || @@ -807,15 +807,15 @@ class StatefulShellRoute extends ShellRouteBase { } static bool _debugValidateRestorationScopeIds( - String? restorationScopeId, List branches) { + String? restorationScopeId, List branches) { if (branches - .map((StatefulShellBranch e) => e.restorationScopeId) + .map((StackedShellBranch e) => e.restorationScopeId) .whereNotNull() .isNotEmpty) { assert( restorationScopeId != null, 'A restorationScopeId must be set for ' - 'the StatefulShellRoute when using restorationScopeIds on one or more ' + 'the StackedShellRoute when using restorationScopeIds on one or more ' 'of the branches'); } return true; @@ -823,24 +823,24 @@ class StatefulShellRoute extends ShellRouteBase { } /// Representation of a separate branch in a stateful navigation tree, used to -/// configure [StatefulShellRoute]. +/// configure [StackedShellRoute]. /// -/// The only required argument when creating a StatefulShellBranch is the +/// The only required argument when creating a StackedShellBranch is the /// 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 [StatefulShellRouteState]). +/// using the goBranch method in [StackedShellRouteState]). /// -/// A separate [Navigator] will be built for each StatefulShellBranch in a -/// [StatefulShellRoute], and the routes of this branch will be placed onto that +/// A separate [Navigator] will be built for each StackedShellBranch in a +/// [StackedShellRoute], and the routes of this branch will be placed onto that /// Navigator instead of the root Navigator. A custom [navigatorKey] can be -/// provided when creating a StatefulShellBranch, which can be useful when the +/// provided when creating a StackedShellBranch, which can be useful when the /// Navigator needs to be accessed elsewhere. If no key is provided, a default /// one will be created. @immutable -class StatefulShellBranch { - /// Constructs a [StatefulShellBranch]. - StatefulShellBranch({ +class StackedShellBranch { + /// Constructs a [StackedShellBranch]. + StackedShellBranch({ required this.routes, GlobalKey? navigatorKey, this.initialLocation, @@ -850,8 +850,8 @@ class StatefulShellBranch { /// The [GlobalKey] to be used by the [Navigator] built for this branch. /// - /// A separate Navigator will be built for each StatefulShellBranch in a - /// [StatefulShellRoute] and this key will be used to identify the Navigator. + /// A separate Navigator will be built for each StackedShellBranch in a + /// [StackedShellRoute] and this key will be used to identify the Navigator. /// The routes associated with this branch will be placed o onto that /// Navigator instead of the root Navigator. final GlobalKey navigatorKey; @@ -865,7 +865,7 @@ class StatefulShellBranch { /// be used (i.e. first element in [routes], or a descendant). The default /// location is used when loading the branch for the first time (for instance /// when switching branch using the goBranch method in - /// [StatefulShellRouteState]). + /// [StackedShellRouteState]). final String? initialLocation; /// Restoration ID to save and restore the state of the navigator, including @@ -880,66 +880,66 @@ class StatefulShellBranch { /// Builder for a custom container for shell route Navigators. typedef ShellNavigatorContainerBuilder = Widget Function(BuildContext context, - StatefulShellRouteState shellRouteState, List children); + StackedShellRouteState shellRouteState, List children); -/// Widget for managing the state of a [StatefulShellRoute]. +/// Widget for managing the state of a [StackedShellRoute]. /// /// 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. +/// internally by StackedShellRoute. However, if a custom container for the +/// branch Navigators is required, StackedNavigationShell can be used in +/// the builder or pageBuilder methods of StackedShellRoute to facilitate this. /// The container is created using the provided [ShellNavigatorContainerBuilder], /// where the List of Widgets represent the Navigators for each branch. /// /// Example: /// ``` -/// builder: (BuildContext context, StatefulShellRouteState state, Widget child) { -/// return StatefulNavigationShell( +/// builder: (BuildContext context, StackedShellRouteState state, Widget child) { +/// return StackedNavigationShell( /// shellRouteState: state, /// containerBuilder: (_, __, List children) => MyCustomShell(shellState: state, children: children), /// ); /// } /// ``` -class StatefulNavigationShell extends StatefulWidget { - /// Constructs an [_StatefulNavigationShell]. - StatefulNavigationShell({ +class StackedNavigationShell extends StatefulWidget { + /// Constructs an [_StackedNavigationShell]. + StackedNavigationShell({ required this.shellRouteState, ShellNavigatorContainerBuilder? containerBuilder, }) : containerBuilder = containerBuilder ?? _defaultChildBuilder, super(key: shellRouteState._shellStateKey); static Widget _defaultChildBuilder(BuildContext context, - StatefulShellRouteState shellRouteState, List children) { + StackedShellRouteState shellRouteState, List children) { return _IndexedStackedRouteBranchContainer( currentIndex: shellRouteState.currentIndex, children: children); } - /// The current state of the associated [StatefulShellRoute]. - final StatefulShellRouteState shellRouteState; + /// The current state of the associated [StackedShellRoute]. + final StackedShellRouteState shellRouteState; /// The builder for a custom container for shell route Navigators. final ShellNavigatorContainerBuilder containerBuilder; @override - State createState() => StatefulNavigationShellState(); + State createState() => StackedNavigationShellState(); } -/// State for StatefulNavigationShell. -class StatefulNavigationShellState extends State +/// State for StackedNavigationShell. +class StackedNavigationShellState extends State with RestorationMixin { final Map _branchNavigators = {}; - StatefulShellRoute get _route => widget.shellRouteState.route; + StackedShellRoute get _route => widget.shellRouteState.route; - StatefulShellRouteState get _routeState => widget.shellRouteState; + StackedShellRouteState get _routeState => widget.shellRouteState; GoRouter get _router => _routeState._router; RouteMatcher get _matcher => _router.routeInformationParser.matcher; GoRouteInformationProvider get _routeInformationProvider => _router.routeInformationProvider; - final Map _branchLocations = - {}; + final Map _branchLocations = + {}; @override String? get restorationId => _route.restorationScopeId; @@ -947,13 +947,13 @@ class StatefulNavigationShellState extends State /// 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 /// always returned (needed for _RestorableRouteMatchList/RestorableValue). - String _branchLocationRestorationScopeId(StatefulShellBranch branch) { + String _branchLocationRestorationScopeId(StackedShellBranch branch) { return branch.restorationScopeId != null ? '${branch.restorationScopeId}-location' : identityHashCode(branch).toString(); } - _RestorableRouteMatchList _branchLocation(StatefulShellBranch branch) { + _RestorableRouteMatchList _branchLocation(StackedShellBranch branch) { return _branchLocations.putIfAbsent(branch, () { final _RestorableRouteMatchList branchLocation = _RestorableRouteMatchList(_matcher); @@ -963,11 +963,11 @@ class StatefulNavigationShellState extends State }); } - Widget? _navigatorForBranch(StatefulShellBranch branch) { + Widget? _navigatorForBranch(StackedShellBranch branch) { return _branchNavigators[branch.navigatorKey]; } - void _setNavigatorForBranch(StatefulShellBranch branch, Widget? navigator) { + void _setNavigatorForBranch(StackedShellBranch branch, Widget? navigator) { navigator != null ? _branchNavigators[branch.navigatorKey] = navigator : _branchNavigators.remove(branch.navigatorKey); @@ -977,8 +977,7 @@ class StatefulNavigationShellState extends State _branchLocations[_route.branches[index]]?.value; void _updateCurrentBranchStateFromWidget() { - final StatefulShellBranch branch = - _route.branches[_routeState.currentIndex]; + final StackedShellBranch branch = _route.branches[_routeState.currentIndex]; final ShellRouteContext shellRouteContext = _routeState._shellRouteContext; /// Create an clone of the current RouteMatchList, to prevent mutations from @@ -1022,7 +1021,7 @@ class StatefulNavigationShellState extends State @override void dispose() { super.dispose(); - for (final StatefulShellBranch branch in _route.branches) { + for (final StackedShellBranch branch in _route.branches) { _branchLocations[branch]?.dispose(); } } @@ -1033,7 +1032,7 @@ class StatefulNavigationShellState extends State } @override - void didUpdateWidget(covariant StatefulNavigationShell oldWidget) { + void didUpdateWidget(covariant StackedNavigationShell oldWidget) { super.didUpdateWidget(oldWidget); _updateCurrentBranchStateFromWidget(); } @@ -1041,7 +1040,7 @@ class StatefulNavigationShellState extends State @override Widget build(BuildContext context) { final List children = _route.branches - .map((StatefulShellBranch branch) => _BranchNavigatorProxy( + .map((StackedShellBranch branch) => _BranchNavigatorProxy( key: ObjectKey(branch), branch: branch, navigatorForBranch: _navigatorForBranch)) @@ -1079,7 +1078,7 @@ class _RestorableRouteMatchList extends RestorableValue { } } -typedef _NavigatorForBranch = Widget? Function(StatefulShellBranch); +typedef _NavigatorForBranch = Widget? Function(StackedShellBranch); /// Widget that serves as the proxy for a branch Navigator Widget, which /// possibly hasn't been created yet. @@ -1096,7 +1095,7 @@ class _BranchNavigatorProxy extends StatefulWidget { required this.navigatorForBranch, }); - final StatefulShellBranch branch; + final StackedShellBranch branch; final _NavigatorForBranch navigatorForBranch; @override @@ -1150,16 +1149,16 @@ class _IndexedStackedRouteBranchContainer extends StatelessWidget { } } -/// The snapshot of the current state of a [StatefulShellRoute]. +/// The snapshot of the current state of a [StackedShellRoute]. /// /// Note that this an immutable class, that represents the snapshot of the state -/// of a StatefulShellRoute at a given point in time. Therefore, instances of +/// of a StackedShellRoute at a given point in time. Therefore, instances of /// this object should not be cached, but instead passed down from the builder -/// functions of StatefulShellRoute. +/// functions of StackedShellRoute. @immutable -class StatefulShellRouteState { - /// Constructs a [_StatefulShellRouteState]. - StatefulShellRouteState._( +class StackedShellRouteState { + /// Constructs a [_StackedShellRouteState]. + StackedShellRouteState._( this._router, this.route, this._shellStateKey, @@ -1169,43 +1168,42 @@ class StatefulShellRouteState { ) : currentIndex = _indexOfBranchNavigatorKey(route, currentNavigatorKey); static int _indexOfBranchNavigatorKey( - StatefulShellRoute route, GlobalKey navigatorKey) { + StackedShellRoute route, GlobalKey navigatorKey) { final int index = route.branches.indexWhere( - (StatefulShellBranch branch) => branch.navigatorKey == navigatorKey); + (StackedShellBranch branch) => branch.navigatorKey == navigatorKey); assert(index >= 0); return index; } final GoRouter _router; - /// The associated [StatefulShellRoute] - final StatefulShellRoute route; + /// The associated [StackedShellRoute] + final StackedShellRoute route; - final GlobalKey _shellStateKey; + final GlobalKey _shellStateKey; - /// The current route state associated with the [StatefulShellRoute]. + /// The current route state associated with the [StackedShellRoute]. final GoRouterState routerState; /// The ShellRouteContext responsible for building the Navigator for the - /// current [StatefulShellBranch] + /// current [StackedShellBranch] final ShellRouteContext _shellRouteContext; - /// The index of the currently active [StatefulShellBranch]. + /// The index of the currently active [StackedShellBranch]. /// /// Corresponds to the index of the branch in the List returned from - /// branchBuilder of [StatefulShellRoute]. + /// branchBuilder of [StackedShellRoute]. final int currentIndex; - /// Navigate to the last location of the [StatefulShellBranch] at the provided - /// index in the associated [StatefulShellBranch]. + /// Navigate to the last location of the [StackedShellBranch] at the provided + /// index in the associated [StackedShellBranch]. /// /// This method will switch the currently active branch [Navigator] for the - /// [StatefulShellRoute]. If the branch has not been visited before, this + /// [StackedShellRoute]. If the branch has not been visited before, this /// method will navigate to initial location of the branch (see - /// [StatefulShellBranch.initialLocation]). + /// [StackedShellBranch.initialLocation]). void goBranch({required int index}) { - final StatefulNavigationShellState? shellState = - _shellStateKey.currentState; + final StackedNavigationShellState? shellState = _shellStateKey.currentState; if (shellState != null) { shellState._goBranch(index); } else { @@ -1220,9 +1218,9 @@ class StatefulShellRouteState { } /// Gets the state for the nearest stateful shell route in the Widget tree. - static StatefulShellRouteState of(BuildContext context) { - final StatefulNavigationShellState? shellState = - context.findAncestorStateOfType(); + static StackedShellRouteState of(BuildContext context) { + final StackedNavigationShellState? shellState = + context.findAncestorStateOfType(); assert(shellState != null); return shellState!._routeState; } @@ -1230,9 +1228,9 @@ class StatefulShellRouteState { /// Gets the state for the nearest stateful shell route in the Widget tree. /// /// Returns null if no stateful shell route is found. - static StatefulShellRouteState? maybeOf(BuildContext context) { - final StatefulNavigationShellState? shellState = - context.findAncestorStateOfType(); + static StackedShellRouteState? maybeOf(BuildContext context) { + final StackedNavigationShellState? shellState = + context.findAncestorStateOfType(); return shellState?._routeState; } } diff --git a/packages/go_router/lib/src/typedefs.dart b/packages/go_router/lib/src/typedefs.dart index de48abd1e761..aeeca23554ba 100644 --- a/packages/go_router/lib/src/typedefs.dart +++ b/packages/go_router/lib/src/typedefs.dart @@ -34,17 +34,17 @@ typedef ShellRoutePageBuilder = Page Function( Widget child, ); -/// The widget builder for [StatefulShellRoute]. -typedef StatefulShellRouteBuilder = Widget Function( +/// The widget builder for [StackedShellRoute]. +typedef StackedShellRouteBuilder = Widget Function( BuildContext context, - StatefulShellRouteState state, + StackedShellRouteState state, Widget child, ); -/// The page builder for [StatefulShellRoute]. -typedef StatefulShellRoutePageBuilder = Page Function( +/// The page builder for [StackedShellRoute]. +typedef StackedShellRoutePageBuilder = Page Function( BuildContext context, - StatefulShellRouteState state, + StackedShellRouteState state, Widget child, ); diff --git a/packages/go_router/test/builder_test.dart b/packages/go_router/test/builder_test.dart index 0f0c9d20845e..9686aad9778a 100644 --- a/packages/go_router/test/builder_test.dart +++ b/packages/go_router/test/builder_test.dart @@ -325,7 +325,7 @@ void main() { 'scope1'); }); - testWidgets('Uses the correct restorationScopeId for StatefulShellRoute', + testWidgets('Uses the correct restorationScopeId for StackedShellRoute', (WidgetTester tester) async { final GlobalKey rootNavigatorKey = GlobalKey(debugLabel: 'root'); @@ -335,13 +335,13 @@ void main() { initialLocation: '/a', navigatorKey: rootNavigatorKey, routes: [ - StatefulShellRoute( + StackedShellRoute( restorationScopeId: 'shell', - builder: (BuildContext context, StatefulShellRouteState state, + builder: (BuildContext context, StackedShellRouteState state, Widget child) => _HomeScreen(child: child), - branches: [ - StatefulShellBranch( + branches: [ + StackedShellBranch( navigatorKey: shellNavigatorKey, restorationScopeId: 'scope1', routes: [ diff --git a/packages/go_router/test/configuration_test.dart b/packages/go_router/test/configuration_test.dart index bf3125cc26f1..db7d202afbba 100644 --- a/packages/go_router/test/configuration_test.dart +++ b/packages/go_router/test/configuration_test.dart @@ -83,7 +83,7 @@ void main() { }); test( - 'throws when StatefulShellRoute sub-route uses incorrect parentNavigatorKey', + 'throws when StackedShellRoute sub-route uses incorrect parentNavigatorKey', () { final GlobalKey root = GlobalKey(debugLabel: 'root'); @@ -97,8 +97,8 @@ void main() { RouteConfiguration( navigatorKey: root, routes: [ - StatefulShellRoute(branches: [ - StatefulShellBranch( + StackedShellRoute(branches: [ + StackedShellBranch( navigatorKey: keyA, routes: [ GoRoute( @@ -112,7 +112,7 @@ void main() { ]), ], ), - ], builder: mockStatefulShellBuilder), + ], builder: mockStackedShellBuilder), ], redirectLimit: 10, topRedirect: (BuildContext context, GoRouterState state) { @@ -125,7 +125,7 @@ void main() { }); test( - 'does not throw when StatefulShellRoute sub-route uses correct parentNavigatorKeys', + 'does not throw when StackedShellRoute sub-route uses correct parentNavigatorKeys', () { final GlobalKey root = GlobalKey(debugLabel: 'root'); @@ -135,8 +135,8 @@ void main() { RouteConfiguration( navigatorKey: root, routes: [ - StatefulShellRoute(branches: [ - StatefulShellBranch( + StackedShellRoute(branches: [ + StackedShellBranch( navigatorKey: keyA, routes: [ GoRoute( @@ -150,7 +150,7 @@ void main() { ]), ], ), - ], builder: mockStatefulShellBuilder), + ], builder: mockStackedShellBuilder), ], redirectLimit: 10, topRedirect: (BuildContext context, GoRouterState state) { @@ -160,7 +160,7 @@ void main() { }); test( - 'throws when a sub-route of StatefulShellRoute has a parentNavigatorKey', + 'throws when a sub-route of StackedShellRoute has a parentNavigatorKey', () { final GlobalKey root = GlobalKey(debugLabel: 'root'); @@ -171,8 +171,8 @@ void main() { RouteConfiguration( navigatorKey: root, routes: [ - StatefulShellRoute(branches: [ - StatefulShellBranch( + StackedShellRoute(branches: [ + StackedShellBranch( routes: [ GoRoute( path: '/a', @@ -185,7 +185,7 @@ void main() { ]), ], ), - StatefulShellBranch( + StackedShellBranch( routes: [ GoRoute( path: '/b', @@ -193,7 +193,7 @@ void main() { parentNavigatorKey: someNavigatorKey), ], ), - ], builder: mockStatefulShellBuilder), + ], builder: mockStackedShellBuilder), ], redirectLimit: 10, topRedirect: (BuildContext context, GoRouterState state) { @@ -205,7 +205,7 @@ void main() { ); }); - test('throws when StatefulShellRoute has duplicate navigator keys', () { + test('throws when StackedShellRoute has duplicate navigator keys', () { final GlobalKey root = GlobalKey(debugLabel: 'root'); final GlobalKey keyA = @@ -221,9 +221,9 @@ void main() { RouteConfiguration( navigatorKey: root, routes: [ - StatefulShellRoute(branches: [ - StatefulShellBranch(routes: shellRouteChildren) - ], builder: mockStatefulShellBuilder), + StackedShellRoute(branches: [ + StackedShellBranch(routes: shellRouteChildren) + ], builder: mockStackedShellBuilder), ], redirectLimit: 10, topRedirect: (BuildContext context, GoRouterState state) { @@ -236,7 +236,7 @@ void main() { }); test( - 'throws when a child of StatefulShellRoute has an incorrect ' + 'throws when a child of StackedShellRoute has an incorrect ' 'parentNavigatorKey', () { final GlobalKey root = GlobalKey(debugLabel: 'root'); @@ -257,14 +257,14 @@ void main() { RouteConfiguration( navigatorKey: root, routes: [ - StatefulShellRoute(branches: [ - StatefulShellBranch( + StackedShellRoute(branches: [ + StackedShellBranch( routes: [routeA], navigatorKey: sectionANavigatorKey), - StatefulShellBranch( + StackedShellBranch( routes: [routeB], navigatorKey: sectionBNavigatorKey), - ], builder: mockStatefulShellBuilder), + ], builder: mockStackedShellBuilder), ], redirectLimit: 10, topRedirect: (BuildContext context, GoRouterState state) { @@ -277,7 +277,7 @@ void main() { }); test( - 'throws when a branch of a StatefulShellRoute has an incorrect ' + 'throws when a branch of a StackedShellRoute has an incorrect ' 'initialLocation', () { final GlobalKey root = GlobalKey(debugLabel: 'root'); @@ -290,8 +290,8 @@ void main() { RouteConfiguration( navigatorKey: root, routes: [ - StatefulShellRoute(branches: [ - StatefulShellBranch( + StackedShellRoute(branches: [ + StackedShellBranch( initialLocation: '/x', navigatorKey: sectionANavigatorKey, routes: [ @@ -301,7 +301,7 @@ void main() { ), ], ), - StatefulShellBranch( + StackedShellBranch( navigatorKey: sectionBNavigatorKey, routes: [ GoRoute( @@ -310,7 +310,7 @@ void main() { ), ], ), - ], builder: mockStatefulShellBuilder), + ], builder: mockStackedShellBuilder), ], redirectLimit: 10, topRedirect: (BuildContext context, GoRouterState state) { @@ -323,7 +323,7 @@ void main() { }); test( - 'throws when a branch of a StatefulShellRoute has a initialLocation ' + 'throws when a branch of a StackedShellRoute has a initialLocation ' 'that is not a descendant of the same branch', () { final GlobalKey root = GlobalKey(debugLabel: 'root'); @@ -336,8 +336,8 @@ void main() { RouteConfiguration( navigatorKey: root, routes: [ - StatefulShellRoute(branches: [ - StatefulShellBranch( + StackedShellRoute(branches: [ + StackedShellBranch( initialLocation: '/b', navigatorKey: sectionANavigatorKey, routes: [ @@ -347,12 +347,12 @@ void main() { ), ], ), - StatefulShellBranch( + StackedShellBranch( initialLocation: '/b', navigatorKey: sectionBNavigatorKey, routes: [ - StatefulShellRoute(branches: [ - StatefulShellBranch( + StackedShellRoute(branches: [ + StackedShellBranch( routes: [ GoRoute( path: '/b', @@ -360,10 +360,10 @@ void main() { ), ], ), - ], builder: mockStatefulShellBuilder), + ], builder: mockStackedShellBuilder), ], ), - ], builder: mockStatefulShellBuilder), + ], builder: mockStackedShellBuilder), ], redirectLimit: 10, topRedirect: (BuildContext context, GoRouterState state) { @@ -376,7 +376,7 @@ void main() { }); test( - 'does not throw when a branch of a StatefulShellRoute has correctly ' + 'does not throw when a branch of a StackedShellRoute has correctly ' 'configured initialLocations', () { final GlobalKey root = GlobalKey(debugLabel: 'root'); @@ -384,8 +384,8 @@ void main() { RouteConfiguration( navigatorKey: root, routes: [ - StatefulShellRoute(branches: [ - StatefulShellBranch( + StackedShellRoute(branches: [ + StackedShellBranch( routes: [ GoRoute( path: '/a', @@ -398,7 +398,7 @@ void main() { ]), ], ), - StatefulShellBranch( + StackedShellBranch( initialLocation: '/b/detail', routes: [ GoRoute( @@ -412,11 +412,11 @@ void main() { ]), ], ), - StatefulShellBranch( + StackedShellBranch( initialLocation: '/c/detail', routes: [ - StatefulShellRoute(branches: [ - StatefulShellBranch( + StackedShellRoute(branches: [ + StackedShellBranch( routes: [ GoRoute( path: '/c', @@ -429,7 +429,7 @@ void main() { ]), ], ), - StatefulShellBranch( + StackedShellBranch( initialLocation: '/d/detail', routes: [ GoRoute( @@ -443,10 +443,10 @@ void main() { ]), ], ), - ], builder: mockStatefulShellBuilder), + ], builder: mockStackedShellBuilder), ], ), - StatefulShellBranch(routes: [ + StackedShellBranch(routes: [ ShellRoute( builder: _mockShellBuilder, routes: [ @@ -462,7 +462,7 @@ void main() { ], ), ]), - ], builder: mockStatefulShellBuilder), + ], builder: mockStackedShellBuilder), ], redirectLimit: 10, topRedirect: (BuildContext context, GoRouterState state) { @@ -472,19 +472,19 @@ void main() { }); test( - 'derives the correct initialLocation for a StatefulShellBranch', + 'derives the correct initialLocation for a StackedShellBranch', () { - final StatefulShellBranch branchA; - final StatefulShellBranch branchY; - final StatefulShellBranch branchB; + final StackedShellBranch branchA; + final StackedShellBranch branchY; + final StackedShellBranch branchB; final RouteConfiguration config = RouteConfiguration( navigatorKey: GlobalKey(debugLabel: 'root'), routes: [ - StatefulShellRoute( - builder: mockStatefulShellBuilder, - branches: [ - branchA = StatefulShellBranch(routes: [ + StackedShellRoute( + builder: mockStackedShellBuilder, + branches: [ + branchA = StackedShellBranch(routes: [ GoRoute( path: '/a', builder: _mockScreenBuilder, @@ -493,11 +493,11 @@ void main() { path: 'x', builder: _mockScreenBuilder, routes: [ - StatefulShellRoute( - builder: mockStatefulShellBuilder, - branches: [ + StackedShellRoute( + builder: mockStackedShellBuilder, + branches: [ branchY = - StatefulShellBranch(routes: [ + StackedShellBranch(routes: [ ShellRoute( builder: _mockShellBuilder, routes: [ @@ -517,7 +517,7 @@ void main() { ], ), ]), - branchB = StatefulShellBranch(routes: [ + branchB = StackedShellBranch(routes: [ ShellRoute( builder: _mockShellBuilder, routes: [ @@ -546,10 +546,10 @@ void main() { }, ); - expect('/a', config.findStatefulShellBranchDefaultLocation(branchA)); + expect('/a', config.findStackedShellBranchDefaultLocation(branchA)); expect( - '/a/x/y1', config.findStatefulShellBranchDefaultLocation(branchY)); - expect('/b1', config.findStatefulShellBranchDefaultLocation(branchB)); + '/a/x/y1', config.findStackedShellBranchDefaultLocation(branchY)); + expect('/b1', config.findStackedShellBranchDefaultLocation(branchB)); }, ); diff --git a/packages/go_router/test/delegate_test.dart b/packages/go_router/test/delegate_test.dart index b532949a17a4..b3afc661a4c0 100644 --- a/packages/go_router/test/delegate_test.dart +++ b/packages/go_router/test/delegate_test.dart @@ -33,15 +33,15 @@ Future createGoRouter( return router; } -Future createGoRouterWithStatefulShellRoute( +Future createGoRouterWithStackedShellRoute( WidgetTester tester) async { final GoRouter router = GoRouter( initialLocation: '/', routes: [ GoRoute(path: '/', builder: (_, __) => const DummyStatefulWidget()), GoRoute(path: '/a', builder: (_, __) => const DummyStatefulWidget()), - StatefulShellRoute(branches: [ - StatefulShellBranch(routes: [ + StackedShellRoute(branches: [ + StackedShellBranch(routes: [ GoRoute( path: '/c', builder: (_, __) => const DummyStatefulWidget(), @@ -54,7 +54,7 @@ Future createGoRouterWithStatefulShellRoute( builder: (_, __) => const DummyStatefulWidget()), ]), ]), - StatefulShellBranch(routes: [ + StackedShellBranch(routes: [ GoRoute( path: '/d', builder: (_, __) => const DummyStatefulWidget(), @@ -64,7 +64,7 @@ Future createGoRouterWithStatefulShellRoute( builder: (_, __) => const DummyStatefulWidget()), ]), ]), - ], builder: mockStatefulShellBuilder), + ], builder: mockStackedShellBuilder), ], ); await tester.pumpWidget(MaterialApp.router( @@ -125,10 +125,10 @@ void main() { testWidgets( 'It should successfully push a route from outside the the current ' - 'StatefulShellRoute', + 'StackedShellRoute', (WidgetTester tester) async { final GoRouter goRouter = - await createGoRouterWithStatefulShellRoute(tester); + await createGoRouterWithStackedShellRoute(tester); goRouter.push('/c/c1'); await tester.pumpAndSettle(); @@ -145,10 +145,10 @@ void main() { testWidgets( 'It should successfully push a route that is a descendant of the current ' - 'StatefulShellRoute branch', + 'StackedShellRoute branch', (WidgetTester tester) async { final GoRouter goRouter = - await createGoRouterWithStatefulShellRoute(tester); + await createGoRouterWithStackedShellRoute(tester); goRouter.push('/c/c1'); await tester.pumpAndSettle(); @@ -164,11 +164,11 @@ void main() { ); testWidgets( - 'It should successfully push the root of the current StatefulShellRoute ' + 'It should successfully push the root of the current StackedShellRoute ' 'branch upon itself', (WidgetTester tester) async { final GoRouter goRouter = - await createGoRouterWithStatefulShellRoute(tester); + await createGoRouterWithStackedShellRoute(tester); goRouter.push('/c'); await tester.pumpAndSettle(); diff --git a/packages/go_router/test/go_router_test.dart b/packages/go_router/test/go_router_test.dart index dda8144d5818..0db210b7702f 100644 --- a/packages/go_router/test/go_router_test.dart +++ b/packages/go_router/test/go_router_test.dart @@ -2445,18 +2445,18 @@ void main() { expect(imperativeRouteMatch.matches.pathParameters['pid'], pid); }); - testWidgets('StatefulShellRoute supports nested routes with params', + testWidgets('StackedShellRoute supports nested routes with params', (WidgetTester tester) async { - StatefulShellRouteState? routeState; + StackedShellRouteState? routeState; final List routes = [ - StatefulShellRoute( - builder: (BuildContext context, StatefulShellRouteState state, + StackedShellRoute( + builder: (BuildContext context, StackedShellRouteState state, Widget child) { routeState = state; return child; }, - branches: [ - StatefulShellBranch( + branches: [ + StackedShellBranch( routes: [ GoRoute( path: '/a', @@ -2465,7 +2465,7 @@ void main() { ), ], ), - StatefulShellBranch( + StackedShellBranch( routes: [ GoRoute( path: '/family', @@ -3016,24 +3016,24 @@ void main() { expect(find.text('Screen C'), findsNothing); }); - testWidgets('Builds StatefulShellRoute', (WidgetTester tester) async { + testWidgets('Builds StackedShellRoute', (WidgetTester tester) async { final GlobalKey rootNavigatorKey = GlobalKey(); final List routes = [ - StatefulShellRoute( - builder: (BuildContext context, StatefulShellRouteState state, + StackedShellRoute( + builder: (BuildContext context, StackedShellRouteState state, Widget child) => child, - branches: [ - StatefulShellBranch(routes: [ + branches: [ + StackedShellBranch(routes: [ GoRoute( path: '/a', builder: (BuildContext context, GoRouterState state) => const Text('Screen A'), ), ]), - StatefulShellBranch(routes: [ + StackedShellBranch(routes: [ GoRoute( path: '/b', builder: (BuildContext context, GoRouterState state) => @@ -3055,7 +3055,7 @@ void main() { expect(find.text('Screen B'), findsOneWidget); }); - testWidgets('Builds StatefulShellRoute as a sub-route', + testWidgets('Builds StackedShellRoute as a sub-route', (WidgetTester tester) async { final GlobalKey rootNavigatorKey = GlobalKey(); @@ -3066,19 +3066,19 @@ void main() { builder: (BuildContext context, GoRouterState state) => const Text('Root'), routes: [ - StatefulShellRoute( - builder: (BuildContext context, StatefulShellRouteState state, + StackedShellRoute( + builder: (BuildContext context, StackedShellRouteState state, Widget child) => child, - branches: [ - StatefulShellBranch(routes: [ + branches: [ + StackedShellBranch(routes: [ GoRoute( path: 'a', builder: (BuildContext context, GoRouterState state) => const Text('Screen A'), ), ]), - StatefulShellBranch(routes: [ + StackedShellBranch(routes: [ GoRoute( path: 'b', builder: (BuildContext context, GoRouterState state) => @@ -3103,23 +3103,23 @@ void main() { }); testWidgets( - 'Navigation with goBranch is correctly handled in StatefulShellRoute', + 'Navigation with goBranch is correctly handled in StackedShellRoute', (WidgetTester tester) async { final GlobalKey rootNavigatorKey = GlobalKey(); final GlobalKey statefulWidgetKey = GlobalKey(); - StatefulShellRouteState? routeState; + StackedShellRouteState? routeState; final List routes = [ - StatefulShellRoute( - builder: (BuildContext context, StatefulShellRouteState state, + StackedShellRoute( + builder: (BuildContext context, StackedShellRouteState state, Widget child) { routeState = state; return child; }, - branches: [ - StatefulShellBranch( + branches: [ + StackedShellBranch( routes: [ GoRoute( path: '/a', @@ -3128,7 +3128,7 @@ void main() { ), ], ), - StatefulShellBranch( + StackedShellBranch( routes: [ GoRoute( path: '/b', @@ -3137,7 +3137,7 @@ void main() { ), ], ), - StatefulShellBranch( + StackedShellBranch( routes: [ GoRoute( path: '/c', @@ -3146,7 +3146,7 @@ void main() { ), ], ), - StatefulShellBranch( + StackedShellBranch( routes: [ GoRoute( path: '/d', @@ -3195,23 +3195,23 @@ void main() { }); testWidgets( - 'Navigates to correct nested navigation tree in StatefulShellRoute ' + 'Navigates to correct nested navigation tree in StackedShellRoute ' 'and maintains state', (WidgetTester tester) async { final GlobalKey rootNavigatorKey = GlobalKey(); final GlobalKey statefulWidgetKey = GlobalKey(); - StatefulShellRouteState? routeState; + StackedShellRouteState? routeState; final List routes = [ - StatefulShellRoute( - builder: (BuildContext context, StatefulShellRouteState state, + StackedShellRoute( + builder: (BuildContext context, StackedShellRouteState state, Widget child) { routeState = state; return child; }, - branches: [ - StatefulShellBranch(routes: [ + branches: [ + StackedShellBranch(routes: [ GoRoute( path: '/a', builder: (BuildContext context, GoRouterState state) => @@ -3228,7 +3228,7 @@ void main() { ], ), ]), - StatefulShellBranch(routes: [ + StackedShellBranch(routes: [ GoRoute( path: '/b', builder: (BuildContext context, GoRouterState state) => @@ -3265,32 +3265,32 @@ void main() { expect(statefulWidgetKey.currentState?.counter, equals(0)); }); - testWidgets('Maintains state for nested StatefulShellRoute', + testWidgets('Maintains state for nested StackedShellRoute', (WidgetTester tester) async { final GlobalKey rootNavigatorKey = GlobalKey(); final GlobalKey statefulWidgetKey = GlobalKey(); - StatefulShellRouteState? routeState1; - StatefulShellRouteState? routeState2; + StackedShellRouteState? routeState1; + StackedShellRouteState? routeState2; final List routes = [ - StatefulShellRoute( - builder: (BuildContext context, StatefulShellRouteState state, + StackedShellRoute( + builder: (BuildContext context, StackedShellRouteState state, Widget child) { routeState1 = state; return child; }, - branches: [ - StatefulShellBranch(routes: [ - StatefulShellRoute( - builder: (BuildContext context, StatefulShellRouteState state, + branches: [ + StackedShellBranch(routes: [ + StackedShellRoute( + builder: (BuildContext context, StackedShellRouteState state, Widget child) { routeState2 = state; return child; }, - branches: [ - StatefulShellBranch(routes: [ + branches: [ + StackedShellBranch(routes: [ GoRoute( path: '/a', builder: (BuildContext context, GoRouterState state) => @@ -3308,14 +3308,14 @@ void main() { ], ), ]), - StatefulShellBranch(routes: [ + StackedShellBranch(routes: [ GoRoute( path: '/b', builder: (BuildContext context, GoRouterState state) => const Text('Screen B'), ), ]), - StatefulShellBranch(routes: [ + StackedShellBranch(routes: [ GoRoute( path: '/c', builder: (BuildContext context, GoRouterState state) => @@ -3324,7 +3324,7 @@ void main() { ]), ]), ]), - StatefulShellBranch(routes: [ + StackedShellBranch(routes: [ GoRoute( path: '/d', builder: (BuildContext context, GoRouterState state) => @@ -3362,7 +3362,7 @@ void main() { }); testWidgets( - 'Pops from the correct Navigator in a StatefulShellRoute when the ' + 'Pops from the correct Navigator in a StackedShellRoute when the ' 'Android back button is pressed', (WidgetTester tester) async { final GlobalKey rootNavigatorKey = GlobalKey(); @@ -3370,17 +3370,17 @@ void main() { GlobalKey(); final GlobalKey sectionBNavigatorKey = GlobalKey(); - StatefulShellRouteState? routeState; + StackedShellRouteState? routeState; final List routes = [ - StatefulShellRoute( - builder: (BuildContext context, StatefulShellRouteState state, + StackedShellRoute( + builder: (BuildContext context, StackedShellRouteState state, Widget child) { routeState = state; return child; }, - branches: [ - StatefulShellBranch( + branches: [ + StackedShellBranch( navigatorKey: sectionANavigatorKey, routes: [ GoRoute( @@ -3396,7 +3396,7 @@ void main() { ], ), ]), - StatefulShellBranch( + StackedShellBranch( navigatorKey: sectionBNavigatorKey, routes: [ GoRoute( @@ -3452,27 +3452,27 @@ void main() { testWidgets( 'Maintains extra navigation information when navigating ' - 'between branches in StatefulShellRoute', (WidgetTester tester) async { + 'between branches in StackedShellRoute', (WidgetTester tester) async { final GlobalKey rootNavigatorKey = GlobalKey(); - StatefulShellRouteState? routeState; + StackedShellRouteState? routeState; final List routes = [ - StatefulShellRoute( - builder: (BuildContext context, StatefulShellRouteState state, + StackedShellRoute( + builder: (BuildContext context, StackedShellRouteState state, Widget child) { routeState = state; return child; }, - branches: [ - StatefulShellBranch(routes: [ + branches: [ + StackedShellBranch(routes: [ GoRoute( path: '/a', builder: (BuildContext context, GoRouterState state) => const Text('Screen A'), ), ]), - StatefulShellBranch(routes: [ + StackedShellBranch(routes: [ GoRoute( path: '/b', builder: (BuildContext context, GoRouterState state) => @@ -3505,11 +3505,11 @@ void main() { testWidgets( 'Pushed non-descendant routes are correctly restored when ' - 'navigating between branches in StatefulShellRoute', + 'navigating between branches in StackedShellRoute', (WidgetTester tester) async { final GlobalKey rootNavigatorKey = GlobalKey(); - StatefulShellRouteState? routeState; + StackedShellRouteState? routeState; final List routes = [ GoRoute( @@ -3517,21 +3517,21 @@ void main() { builder: (BuildContext context, GoRouterState state) => Text('Common - ${state.extra}'), ), - StatefulShellRoute( - builder: (BuildContext context, StatefulShellRouteState state, + StackedShellRoute( + builder: (BuildContext context, StackedShellRouteState state, Widget child) { routeState = state; return child; }, - branches: [ - StatefulShellBranch(routes: [ + branches: [ + StackedShellBranch(routes: [ GoRoute( path: '/a', builder: (BuildContext context, GoRouterState state) => const Text('Screen A'), ), ]), - StatefulShellBranch(routes: [ + StackedShellBranch(routes: [ GoRoute( path: '/b', builder: (BuildContext context, GoRouterState state) => @@ -3570,27 +3570,27 @@ void main() { testWidgets( 'Redirects are correctly handled when switching branch in a ' - 'StatefulShellRoute', (WidgetTester tester) async { + 'StackedShellRoute', (WidgetTester tester) async { final GlobalKey rootNavigatorKey = GlobalKey(); - StatefulShellRouteState? routeState; + StackedShellRouteState? routeState; final List routes = [ - StatefulShellRoute( - builder: (BuildContext context, StatefulShellRouteState state, + StackedShellRoute( + builder: (BuildContext context, StackedShellRouteState state, Widget child) { routeState = state; return child; }, - branches: [ - StatefulShellBranch(routes: [ + branches: [ + StackedShellBranch(routes: [ GoRoute( path: '/a', builder: (BuildContext context, GoRouterState state) => const Text('Screen A'), ), ]), - StatefulShellBranch(routes: [ + StackedShellBranch(routes: [ GoRoute( path: '/b', builder: (BuildContext context, GoRouterState state) => @@ -3609,7 +3609,7 @@ void main() { ], ), ]), - StatefulShellBranch(routes: [ + StackedShellBranch(routes: [ GoRoute( path: '/c', redirect: (_, __) => '/c/main2', @@ -3783,7 +3783,7 @@ void main() { ); testWidgets( - 'It checks if StatefulShellRoute navigators can pop', + 'It checks if StackedShellRoute navigators can pop', (WidgetTester tester) async { final GlobalKey rootNavigatorKey = GlobalKey(); @@ -3791,10 +3791,10 @@ void main() { navigatorKey: rootNavigatorKey, initialLocation: '/a', routes: [ - StatefulShellRoute( - builder: mockStatefulShellBuilder, - branches: [ - StatefulShellBranch(routes: [ + StackedShellRoute( + builder: mockStackedShellBuilder, + branches: [ + StackedShellBranch(routes: [ GoRoute( path: '/a', builder: (BuildContext context, _) { @@ -3804,7 +3804,7 @@ void main() { }, ), ]), - StatefulShellBranch(routes: [ + StackedShellBranch(routes: [ GoRoute( path: '/b', builder: (BuildContext context, _) { @@ -3844,7 +3844,7 @@ void main() { expect(find.text('Screen B detail', skipOffstage: false), findsOneWidget); expect(router.canPop(), true); - // Verify that it is actually the StatefulShellRoute that reports + // Verify that it is actually the StackedShellRoute that reports // canPop = true expect(rootNavigatorKey.currentState?.canPop(), false); }, @@ -4278,7 +4278,7 @@ void main() { equals('/pushed-p1')); }); - testWidgets('Restores state of branches in StatefulShellRoute correctly', + testWidgets('Restores state of branches in StackedShellRoute correctly', (WidgetTester tester) async { final GlobalKey rootNavigatorKey = GlobalKey(); @@ -4288,65 +4288,59 @@ void main() { GlobalKey(); final GlobalKey statefulWidgetKeyC = GlobalKey(); - StatefulShellRouteState? routeState; + StackedShellRouteState? routeState; final List routes = [ - StatefulShellRoute( + StackedShellRoute( restorationScopeId: 'shell', - pageBuilder: (BuildContext context, StatefulShellRouteState state, + pageBuilder: (BuildContext context, StackedShellRouteState state, Widget child) { routeState = state; return MaterialPage( restorationId: 'shellWidget', child: child); }, - branches: [ - StatefulShellBranch( - restorationScopeId: 'branchA', - routes: [ + branches: [ + StackedShellBranch(restorationScopeId: 'branchA', routes: [ + GoRoute( + path: '/a', + pageBuilder: createPageBuilder( + restorationId: 'screenA', child: const Text('Screen A')), + routes: [ GoRoute( - path: '/a', + path: 'detailA', pageBuilder: createPageBuilder( - restorationId: 'screenA', - child: const Text('Screen A')), - routes: [ - GoRoute( - path: 'detailA', - pageBuilder: createPageBuilder( - restorationId: 'screenADetail', - child: Column(children: [ - const Text('Screen A Detail'), - DummyRestorableStatefulWidget( - key: statefulWidgetKeyA, - restorationId: 'counterA'), - ])), - ), - ], + restorationId: 'screenADetail', + child: Column(children: [ + const Text('Screen A Detail'), + DummyRestorableStatefulWidget( + key: statefulWidgetKeyA, + restorationId: 'counterA'), + ])), ), - ]), - StatefulShellBranch( - restorationScopeId: 'branchB', - routes: [ + ], + ), + ]), + StackedShellBranch(restorationScopeId: 'branchB', routes: [ + GoRoute( + path: '/b', + pageBuilder: createPageBuilder( + restorationId: 'screenB', child: const Text('Screen B')), + routes: [ GoRoute( - path: '/b', + path: 'detailB', pageBuilder: createPageBuilder( - restorationId: 'screenB', - child: const Text('Screen B')), - routes: [ - GoRoute( - path: 'detailB', - pageBuilder: createPageBuilder( - restorationId: 'screenBDetail', - child: Column(children: [ - const Text('Screen B Detail'), - DummyRestorableStatefulWidget( - key: statefulWidgetKeyB, - restorationId: 'counterB'), - ])), - ), - ], + restorationId: 'screenBDetail', + child: Column(children: [ + const Text('Screen B Detail'), + DummyRestorableStatefulWidget( + key: statefulWidgetKeyB, + restorationId: 'counterB'), + ])), ), - ]), - StatefulShellBranch(routes: [ + ], + ), + ]), + StackedShellBranch(routes: [ GoRoute( path: '/c', pageBuilder: createPageBuilder( @@ -4411,7 +4405,7 @@ void main() { }); testWidgets( - 'Restores state of imperative routes in StatefulShellRoute correctly', + 'Restores state of imperative routes in StackedShellRoute correctly', (WidgetTester tester) async { final GlobalKey rootNavigatorKey = GlobalKey(); @@ -4419,55 +4413,52 @@ void main() { GlobalKey(); final GlobalKey statefulWidgetKeyB = GlobalKey(); - StatefulShellRouteState? routeStateRoot; - StatefulShellRouteState? routeStateNested; + StackedShellRouteState? routeStateRoot; + StackedShellRouteState? routeStateNested; final List routes = [ - StatefulShellRoute( + StackedShellRoute( restorationScopeId: 'shell', - pageBuilder: (BuildContext context, StatefulShellRouteState state, + pageBuilder: (BuildContext context, StackedShellRouteState state, Widget child) { routeStateRoot = state; return MaterialPage( restorationId: 'shellWidget', child: child); }, - branches: [ - StatefulShellBranch( - restorationScopeId: 'branchA', - routes: [ + branches: [ + StackedShellBranch(restorationScopeId: 'branchA', routes: [ + GoRoute( + path: '/a', + pageBuilder: createPageBuilder( + restorationId: 'screenA', child: const Text('Screen A')), + routes: [ GoRoute( - path: '/a', + path: 'detailA', pageBuilder: createPageBuilder( - restorationId: 'screenA', - child: const Text('Screen A')), - routes: [ - GoRoute( - path: 'detailA', - pageBuilder: createPageBuilder( - restorationId: 'screenADetail', - child: Column(children: [ - const Text('Screen A Detail'), - DummyRestorableStatefulWidget( - key: statefulWidgetKeyA, - restorationId: 'counterA'), - ])), - ), - ], + restorationId: 'screenADetail', + child: Column(children: [ + const Text('Screen A Detail'), + DummyRestorableStatefulWidget( + key: statefulWidgetKeyA, + restorationId: 'counterA'), + ])), ), - ]), - StatefulShellBranch( + ], + ), + ]), + StackedShellBranch( restorationScopeId: 'branchB', routes: [ - StatefulShellRoute( + StackedShellRoute( restorationScopeId: 'branchB-nested-shell', pageBuilder: (BuildContext context, - StatefulShellRouteState state, Widget child) { + StackedShellRouteState state, Widget child) { routeStateNested = state; return MaterialPage( restorationId: 'shellWidget-nested', child: child); }, - branches: [ - StatefulShellBranch( + branches: [ + StackedShellBranch( restorationScopeId: 'branchB-nested', routes: [ GoRoute( @@ -4501,7 +4492,7 @@ void main() { // ])), // ), ]), - StatefulShellBranch( + StackedShellBranch( restorationScopeId: 'branchC-nested', routes: [ GoRoute( diff --git a/packages/go_router/test/test_helpers.dart b/packages/go_router/test/test_helpers.dart index 2ac60c06f691..8e1e8cde36b7 100644 --- a/packages/go_router/test/test_helpers.dart +++ b/packages/go_router/test/test_helpers.dart @@ -292,8 +292,8 @@ GoRouterPageBuilder createPageBuilder( (BuildContext context, GoRouterState state) => MaterialPage(restorationId: restorationId, child: child); -StatefulShellRouteBuilder mockStatefulShellBuilder = - (BuildContext context, StatefulShellRouteState state, Widget child) { +StackedShellRouteBuilder mockStackedShellBuilder = + (BuildContext context, StackedShellRouteState state, Widget child) { return child; }; From ec6722cab861baadf2831a6f6d7802bfdcba2f6f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20L=C3=B6fstrand?= Date: Sat, 15 Apr 2023 22:38:45 +0200 Subject: [PATCH 095/112] Update packages/go_router/lib/src/route.dart Co-authored-by: chunhtai <47866232+chunhtai@users.noreply.github.com> --- packages/go_router/lib/src/route.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/go_router/lib/src/route.dart b/packages/go_router/lib/src/route.dart index 62143c83b7f3..fd3c27996990 100644 --- a/packages/go_router/lib/src/route.dart +++ b/packages/go_router/lib/src/route.dart @@ -356,7 +356,7 @@ class ShellRouteContext { required this.navigatorBuilder, }); - /// The current immediate sub-route of the associated shell route. + /// The matched immediate sub-route of the associated shell route. final RouteBase subRoute; /// The route match list for the current route. From 9bb0da9fefc71200f2ff6ae5e629cec8228b779f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20L=C3=B6fstrand?= Date: Sat, 15 Apr 2023 22:39:15 +0200 Subject: [PATCH 096/112] Update packages/go_router/lib/src/route.dart Co-authored-by: chunhtai <47866232+chunhtai@users.noreply.github.com> --- packages/go_router/lib/src/route.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/go_router/lib/src/route.dart b/packages/go_router/lib/src/route.dart index fd3c27996990..fb89f98d29f9 100644 --- a/packages/go_router/lib/src/route.dart +++ b/packages/go_router/lib/src/route.dart @@ -359,7 +359,7 @@ class ShellRouteContext { /// The matched immediate sub-route of the associated shell route. final RouteBase subRoute; - /// The route match list for the current route. + /// The route match list for the current location. final RouteMatchList routeMatchList; /// The navigator builder. From a1c7a8f4637739ea2533ab766910c645fbe0b99a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20L=C3=B6fstrand?= Date: Sat, 15 Apr 2023 22:39:48 +0200 Subject: [PATCH 097/112] Update packages/go_router/lib/src/route.dart Co-authored-by: chunhtai <47866232+chunhtai@users.noreply.github.com> --- packages/go_router/lib/src/route.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/go_router/lib/src/route.dart b/packages/go_router/lib/src/route.dart index fb89f98d29f9..a500201c0206 100644 --- a/packages/go_router/lib/src/route.dart +++ b/packages/go_router/lib/src/route.dart @@ -681,7 +681,7 @@ class StackedShellRoute extends ShellRouteBase { this.restorationScopeId, }) : assert(branches.isNotEmpty), assert((pageBuilder != null) ^ (builder != null), - 'builder or pageBuilder must be provided'), + 'One of builder or pageBuilder must be provided, but not both'), assert(_debugUniqueNavigatorKeys(branches).length == branches.length, 'Navigator keys must be unique'), assert(_debugValidateParentNavigatorKeys(branches)), From 477b47c9f7b09153fa83612a2b55078d3fd844b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20Lo=CC=88fstrand?= Date: Thu, 20 Apr 2023 16:52:16 +0200 Subject: [PATCH 098/112] Removed StackedShellRouteState and moved functionality into StackedNavigationShell. Added navigatorContainerBuilder field to StackedShellRoute. Added locationForRoute to GoRouter (and RouteConfiguration). Various other refactoring due to review feedback. --- .../stacked_shell_state_restoration.dart | 59 ++- .../example/lib/stacked_shell_route.dart | 73 ++-- packages/go_router/lib/go_router.dart | 6 +- packages/go_router/lib/src/builder.dart | 5 +- packages/go_router/lib/src/configuration.dart | 132 +++--- packages/go_router/lib/src/matching.dart | 2 +- packages/go_router/lib/src/path_utils.dart | 23 + packages/go_router/lib/src/route.dart | 397 +++++++++--------- packages/go_router/lib/src/router.dart | 7 + packages/go_router/lib/src/typedefs.dart | 8 +- packages/go_router/test/builder_test.dart | 7 +- .../go_router/test/configuration_test.dart | 12 +- packages/go_router/test/go_router_test.dart | 209 +++++---- packages/go_router/test/match_test.dart | 2 +- packages/go_router/test/test_helpers.dart | 6 +- 15 files changed, 503 insertions(+), 445 deletions(-) diff --git a/packages/go_router/example/lib/others/stacked_shell_state_restoration.dart b/packages/go_router/example/lib/others/stacked_shell_state_restoration.dart index f43024011433..4a246f655d0a 100644 --- a/packages/go_router/example/lib/others/stacked_shell_state_restoration.dart +++ b/packages/go_router/example/lib/others/stacked_shell_state_restoration.dart @@ -125,28 +125,31 @@ class RestorableStackedShellRouteExampleApp extends StatelessWidget { ), ]), ], - pageBuilder: (BuildContext context, - StackedShellRouteState state, Widget child) { + pageBuilder: (BuildContext context, GoRouterState state, + StackedNavigationShell navigationShell) { return MaterialPage( - restorationId: 'shellWidget2', - child: StackedNavigationShell( - shellRouteState: state, - containerBuilder: (BuildContext context, - StackedShellRouteState state, - List children) => - TabbedRootScreen( - shellState: state, children: children), - )); + restorationId: 'shellWidget2', child: navigationShell); }, + navigatorContainerBuilder: (BuildContext context, + StackedNavigationShell 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 used in the TabBarView. + TabbedRootScreen( + navigationShell: navigationShell, children: children), ), ], ), ], - pageBuilder: - (BuildContext context, StackedShellRouteState state, Widget child) { + pageBuilder: (BuildContext context, GoRouterState state, + StackedNavigationShell navigationShell) { return MaterialPage( restorationId: 'shellWidget1', - child: ScaffoldWithNavBar(shellState: state, body: child)); + child: ScaffoldWithNavBar(navigationShell: navigationShell)); }, ), ], @@ -170,28 +173,24 @@ class RestorableStackedShellRouteExampleApp extends StatelessWidget { class ScaffoldWithNavBar extends StatelessWidget { /// Constructs an [ScaffoldWithNavBar]. const ScaffoldWithNavBar({ - required this.shellState, - required this.body, + required this.navigationShell, Key? key, }) : super(key: key ?? const ValueKey('ScaffoldWithNavBar')); - /// The current state of the parent StackedShellRoute. - final StackedShellRouteState shellState; - - /// Body, i.e. the container for the branch Navigators. - final Widget body; + /// The navigation shell and container for the branch Navigators. + final StackedNavigationShell navigationShell; @override Widget build(BuildContext context) { return Scaffold( - body: body, + body: navigationShell, bottomNavigationBar: BottomNavigationBar( items: const [ BottomNavigationBarItem(icon: Icon(Icons.home), label: 'Section A'), - BottomNavigationBarItem(icon: Icon(Icons.tab), label: 'Section B'), + BottomNavigationBarItem(icon: Icon(Icons.work), label: 'Section B'), ], - currentIndex: shellState.currentIndex, - onTap: (int tappedIndex) => shellState.goBranch(index: tappedIndex), + currentIndex: navigationShell.currentIndex, + onTap: (int tappedIndex) => navigationShell.goBranch(tappedIndex), ), ); } @@ -344,10 +343,10 @@ class DetailsScreenState extends State with RestorationMixin { class TabbedRootScreen extends StatefulWidget { /// Constructs a TabbedRootScreen const TabbedRootScreen( - {required this.shellState, required this.children, super.key}); + {required this.navigationShell, required this.children, super.key}); /// The current state of the parent StackedShellRoute. - final StackedShellRouteState shellState; + final StackedNavigationShell navigationShell; /// The children (Navigators) to display in the [TabBarView]. final List children; @@ -361,12 +360,12 @@ class _TabbedRootScreenState extends State late final TabController _tabController = TabController( length: widget.children.length, vsync: this, - initialIndex: widget.shellState.currentIndex); + initialIndex: widget.navigationShell.currentIndex); @override void didUpdateWidget(covariant TabbedRootScreen oldWidget) { super.didUpdateWidget(oldWidget); - _tabController.index = widget.shellState.currentIndex; + _tabController.index = widget.navigationShell.currentIndex; } @override @@ -391,7 +390,7 @@ class _TabbedRootScreenState extends State } void _onTabTap(BuildContext context, int index) { - widget.shellState.goBranch(index: index); + widget.navigationShell.goBranch(index); } } diff --git a/packages/go_router/example/lib/stacked_shell_route.dart b/packages/go_router/example/lib/stacked_shell_route.dart index 5883d5872a8d..d50c6a4f6578 100644 --- a/packages/go_router/example/lib/stacked_shell_route.dart +++ b/packages/go_router/example/lib/stacked_shell_route.dart @@ -150,44 +150,47 @@ class NestedTabNavigationExampleApp extends StatelessWidget { ), ]), ], - builder: (BuildContext context, StackedShellRouteState state, - Widget child) { + builder: (BuildContext context, GoRouterState state, + StackedNavigationShell navigationShell) { /// This nested StackedShellRoute demonstrates the use of a /// custom container (TabBarView) for the branch Navigators. - /// Here, the default branch Navigator container (`child`) is - /// ignored, and the class StackedNavigationShell is - /// instead used to provide access to the widgets representing - /// the branch Navigators (`List children`). - /// - /// See TabbedRootScreen for more details on how the children - /// are used in the TabBarView. - return StackedNavigationShell( - shellRouteState: state, - containerBuilder: (BuildContext context, - StackedShellRouteState state, - List children) => - TabbedRootScreen(shellState: state, children: children), - ); + /// 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, + StackedNavigationShell 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 used in the TabBarView. + TabbedRootScreen( + navigationShell: navigationShell, children: children), ), ], ), ], - builder: - (BuildContext context, StackedShellRouteState state, Widget child) { + builder: (BuildContext context, GoRouterState state, + StackedNavigationShell navigationShell) { /// This builder implementation uses the default container for the /// branch Navigators (provided in through the `child` argument). This /// is the simplest way to use StackedShellRoute, where the shell is /// built around the Navigator container (see ScaffoldWithNavBar). - return ScaffoldWithNavBar(shellState: state, body: child); + return ScaffoldWithNavBar(navigationShell: navigationShell); }, /// If it's necessary to customize the Page for StackedShellRoute, /// provide a pageBuilder function instead of the builder, for example: - // pageBuilder: (BuildContext context, StackedShellRouteState state, - // Widget child) { + // pageBuilder: (BuildContext context, GoRouterState state, + // StackedNavigationShell navigationShell) { // return NoTransitionPage( - // child: ScaffoldWithNavBar(shellState: state, body: child)); + // child: ScaffoldWithNavBar(navigationShell: navigationShell)); // }, ), ], @@ -210,29 +213,25 @@ class NestedTabNavigationExampleApp extends StatelessWidget { class ScaffoldWithNavBar extends StatelessWidget { /// Constructs an [ScaffoldWithNavBar]. const ScaffoldWithNavBar({ - required this.shellState, - required this.body, + required this.navigationShell, Key? key, }) : super(key: key ?? const ValueKey('ScaffoldWithNavBar')); - /// The current state of the parent StackedShellRoute. - final StackedShellRouteState shellState; - - /// Body, i.e. the container for the branch Navigators. - final Widget body; + /// The navigation shell and container for the branch Navigators. + final StackedNavigationShell navigationShell; @override Widget build(BuildContext context) { return Scaffold( - body: body, + body: navigationShell, bottomNavigationBar: BottomNavigationBar( 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.currentIndex, - onTap: (int tappedIndex) => shellState.goBranch(index: tappedIndex), + currentIndex: navigationShell.currentIndex, + onTap: (int tappedIndex) => navigationShell.goBranch(tappedIndex), ), ); } @@ -459,10 +458,10 @@ class ModalScreen extends StatelessWidget { class TabbedRootScreen extends StatefulWidget { /// Constructs a TabbedRootScreen const TabbedRootScreen( - {required this.shellState, required this.children, super.key}); + {required this.navigationShell, required this.children, super.key}); /// The current state of the parent StackedShellRoute. - final StackedShellRouteState shellState; + final StackedNavigationShell navigationShell; /// The children (Navigators) to display in the [TabBarView]. final List children; @@ -476,12 +475,12 @@ class _TabbedRootScreenState extends State late final TabController _tabController = TabController( length: widget.children.length, vsync: this, - initialIndex: widget.shellState.currentIndex); + initialIndex: widget.navigationShell.currentIndex); @override void didUpdateWidget(covariant TabbedRootScreen oldWidget) { super.didUpdateWidget(oldWidget); - _tabController.index = widget.shellState.currentIndex; + _tabController.index = widget.navigationShell.currentIndex; } @override @@ -506,7 +505,7 @@ class _TabbedRootScreenState extends State } void _onTabTap(BuildContext context, int index) { - widget.shellState.goBranch(index: index); + widget.navigationShell.goBranch(index); } } diff --git a/packages/go_router/lib/go_router.dart b/packages/go_router/lib/go_router.dart index 5d377f427715..7a02fd3f3abb 100644 --- a/packages/go_router/lib/go_router.dart +++ b/packages/go_router/lib/go_router.dart @@ -12,11 +12,11 @@ export 'src/configuration.dart' GoRouterState, RouteBase, ShellRoute, - ShellNavigatorContainerBuilder, + StackedNavigationContainerBuilder, StackedNavigationShell, + StackedNavigationShellState, StackedShellBranch, - StackedShellRoute, - StackedShellRouteState; + StackedShellRoute; export 'src/misc/extensions.dart'; export 'src/misc/inherited_router.dart'; export 'src/pages/custom_transition_page.dart'; diff --git a/packages/go_router/lib/src/builder.dart b/packages/go_router/lib/src/builder.dart index f14a12b9533a..d937d2ce18aa 100644 --- a/packages/go_router/lib/src/builder.dart +++ b/packages/go_router/lib/src/builder.dart @@ -166,7 +166,7 @@ class RouteBuilder { Set> _nestedStatefulNavigatorKeys( Iterable routes) { - return RouteConfiguration.routesRecursively(routes) + return RouteBase.routesRecursively(routes) .whereType() .expand((StackedShellRoute e) => e.branches.map((StackedShellBranch b) => b.navigatorKey)) @@ -254,7 +254,10 @@ class RouteBuilder { // Call the ShellRouteBase to create/update the shell route state final ShellRouteContext shellRouteContext = ShellRouteContext( + route: route, subRoute: subRoute, + routerState: state, + navigatorKey: shellNavigatorKey, routeMatchList: matchList, navigatorBuilder: buildShellNavigator, ); diff --git a/packages/go_router/lib/src/configuration.dart b/packages/go_router/lib/src/configuration.dart index 806356a4c6e0..233f5e0baad5 100644 --- a/packages/go_router/lib/src/configuration.dart +++ b/packages/go_router/lib/src/configuration.dart @@ -141,7 +141,13 @@ class RouteConfiguration { if (branch.initialLocation == null) { // Recursively search for the first GoRoute descendant. Will // throw assertion error if not found. - findStackedShellBranchDefaultLocation(branch); + final GoRoute? route = branch.defaultRoute; + final String? initialLocation = + route != null ? locationForRoute(route) : null; + assert( + initialLocation != null, + 'The initial location of a StackedShellBranch must be ' + 'derivable from GoRoute descendant'); } else { final RouteBase initialLocationRoute = matcher.findMatch(branch.initialLocation!).last.route; @@ -167,70 +173,75 @@ class RouteConfiguration { return true; } - /// Returns an Iterable that traverses the provided routes and their - /// sub-routes recursively. - static Iterable routesRecursively( - Iterable routes) sync* { - for (final RouteBase route in routes) { - yield route; - yield* routesRecursively(route.routes); - } - } + // /// Returns an Iterable that traverses the provided routes and their + // /// sub-routes recursively. + // static Iterable routesRecursively( + // Iterable routes) sync* { + // for (final RouteBase route in routes) { + // yield route; + // yield* routesRecursively(route.routes); + // } + // } + // + // static Iterable routesRecursively2(Iterable routes) { + // return routes.expand((RouteBase e) => [e, ...routesRecursively2(e.routes)]); + // } - static GoRoute? _findFirstGoRoute(List routes) => - routesRecursively(routes).whereType().firstOrNull; + // static GoRoute? _findFirstGoRoute(List routes) => + // routesRecursively(routes).whereType().firstOrNull; /// Tests if a route is a descendant of, or same as, an ancestor route. bool _debugIsDescendantOrSame( {required RouteBase ancestor, required RouteBase route}) => - ancestor == route || routesRecursively(ancestor.routes).contains(route); + ancestor == route || + RouteBase.routesRecursively(ancestor.routes).contains(route); - /// Recursively traverses the routes of the provided StackedShellBranch to - /// find the first GoRoute, from which a full path will be derived. - String findStackedShellBranchDefaultLocation(StackedShellBranch branch) { - final GoRoute? route = _findFirstGoRoute(branch.routes); - final String? initialLocation = - route != null ? _fullPathForRoute(route, '', routes) : null; - assert( - initialLocation != null, - 'The initial location of a StackedShellBranch must be derivable from ' - 'GoRoute descendant'); - return initialLocation!; - } - - /// Returns the effective initial location of a StackedShellBranch. - /// - /// If the initial location of the branch is null, - /// [findStackedShellBranchDefaultLocation] is used to calculate the initial - /// location. - String effectiveInitialBranchLocation(StackedShellBranch branch) { - final String? initialLocation = branch.initialLocation; - if (initialLocation != null) { - return initialLocation; - } else { - return findStackedShellBranchDefaultLocation(branch); - } - } + // /// Recursively traverses the routes of the provided StackedShellBranch to + // /// find the first GoRoute, from which a full path will be derived. + // String findStackedShellBranchDefaultLocation(StackedShellBranch branch) { + // final GoRoute? route = _findFirstGoRoute(branch.routes); + // final String? initialLocation = + // route != null ? fullPathForRoute(route, '', routes) : null; + // assert( + // initialLocation != null, + // 'The initial location of a StackedShellBranch must be derivable from ' + // 'GoRoute descendant'); + // return initialLocation!; + // } - static String? _fullPathForRoute( - RouteBase targetRoute, String parentFullpath, List routes) { - for (final RouteBase route in routes) { - final String fullPath = (route is GoRoute) - ? concatenatePaths(parentFullpath, route.path) - : parentFullpath; + // /// Returns the effective initial location of a StackedShellBranch. + // /// + // /// If the initial location of the branch is null, + // /// [findStackedShellBranchDefaultLocation] is used to calculate the initial + // /// location. + // String effectiveInitialBranchLocation(StackedShellBranch branch) { + // final String? initialLocation = branch.initialLocation; + // if (initialLocation != null) { + // return initialLocation; + // } else { + // return findStackedShellBranchDefaultLocation(branch); + // } + // } - if (route == targetRoute) { - return fullPath; - } else { - final String? subRoutePath = - _fullPathForRoute(targetRoute, fullPath, route.routes); - if (subRoutePath != null) { - return subRoutePath; - } - } - } - return null; - } + // static String? _fullPathForRoute( + // RouteBase targetRoute, String parentFullpath, List routes) { + // for (final RouteBase route in routes) { + // final String fullPath = (route is GoRoute) + // ? concatenatePaths(parentFullpath, route.path) + // : parentFullpath; + // + // if (route == targetRoute) { + // return fullPath; + // } else { + // final String? subRoutePath = + // _fullPathForRoute(targetRoute, fullPath, route.routes); + // if (subRoutePath != null) { + // return subRoutePath; + // } + // } + // } + // return null; + // } /// The list of top level routes used by [GoRouterDelegate]. final List routes; @@ -288,6 +299,13 @@ class RouteConfiguration { .toString(); } + /// Get the location for the provided route. + /// + /// Builds the absolute path for the route, by concatenating the paths of the + /// route and all its ancestors. + String? locationForRoute(RouteBase route) => + fullPathForRoute(route, '', routes); + @override String toString() { return 'RouterConfiguration: $routes'; diff --git a/packages/go_router/lib/src/matching.dart b/packages/go_router/lib/src/matching.dart index 44e755a57191..c8438c87d5d3 100644 --- a/packages/go_router/lib/src/matching.dart +++ b/packages/go_router/lib/src/matching.dart @@ -62,7 +62,7 @@ class RouteMatchList { /// Creates a copy of this RouteMatchList that can be modified without /// affecting the original. - RouteMatchList clone() { + RouteMatchList copy() { return RouteMatchList._(List.from(_matches), _uri, Map.from(pathParameters), fullpath); } diff --git a/packages/go_router/lib/src/path_utils.dart b/packages/go_router/lib/src/path_utils.dart index e9db923d71c5..ba403f478334 100644 --- a/packages/go_router/lib/src/path_utils.dart +++ b/packages/go_router/lib/src/path_utils.dart @@ -2,6 +2,8 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import '../go_router.dart'; + final RegExp _parameterRegExp = RegExp(r':(\w+)(\((?:\\.|[^\\()])+\))?'); /// Converts a [pattern] such as `/user/:id` into [RegExp]. @@ -135,3 +137,24 @@ String canonicalUri(String loc) { return canon; } + +/// Builds an absolute path for the provided route. +String? fullPathForRoute( + RouteBase targetRoute, String parentFullpath, List routes) { + for (final RouteBase route in routes) { + final String fullPath = (route is GoRoute) + ? concatenatePaths(parentFullpath, route.path) + : parentFullpath; + + if (route == targetRoute) { + return fullPath; + } else { + final String? subRoutePath = + fullPathForRoute(targetRoute, fullPath, route.routes); + if (subRoutePath != null) { + return subRoutePath; + } + } + } + return null; +} diff --git a/packages/go_router/lib/src/route.dart b/packages/go_router/lib/src/route.dart index a500201c0206..8649dce253b9 100644 --- a/packages/go_router/lib/src/route.dart +++ b/packages/go_router/lib/src/route.dart @@ -7,7 +7,6 @@ import 'package:flutter/widgets.dart'; import 'package:meta/meta.dart'; import '../go_router.dart'; -import 'information_provider.dart'; import 'matching.dart'; import 'path_utils.dart'; import 'typedefs.dart'; @@ -103,6 +102,13 @@ abstract class RouteBase { /// The list of child routes associated with this route. final List routes; + + /// Builds a lists containing the provided routes along with all their + /// descendant [routes]. + static Iterable routesRecursively(Iterable routes) { + return routes.expand( + (RouteBase e) => [e, ...routesRecursively(e.routes)]); + } } /// A route that is displayed visually above the matching parent route using the @@ -351,30 +357,32 @@ abstract class ShellRouteBase extends RouteBase { class ShellRouteContext { /// Constructs a [ShellRouteContext]. ShellRouteContext({ + required this.route, required this.subRoute, + required this.routerState, + required this.navigatorKey, required this.routeMatchList, required this.navigatorBuilder, }); + /// The associated shell route. + final ShellRouteBase route; + /// The matched immediate sub-route of the associated shell route. final RouteBase subRoute; + /// 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 for the current location. final RouteMatchList routeMatchList; - /// The navigator builder. + /// Function used to build the [Navigator] for the current route. final NavigatorBuilder navigatorBuilder; - - /// Builds the [Navigator] for the current route. - Widget buildNavigator({ - List? observers, - String? restorationScopeId, - }) { - return navigatorBuilder( - observers, - restorationScopeId, - ); - } } /// A route that displays a UI shell around the matching child route. @@ -512,8 +520,8 @@ class ShellRoute extends ShellRouteBase { Widget? buildWidget(BuildContext context, GoRouterState state, ShellRouteContext shellRouteContext) { if (builder != null) { - final Widget navigator = shellRouteContext.buildNavigator( - observers: observers, restorationScopeId: restorationScopeId); + final Widget navigator = + shellRouteContext.navigatorBuilder(observers, restorationScopeId); return builder!(context, state, navigator); } return null; @@ -523,8 +531,8 @@ class ShellRoute extends ShellRouteBase { Page? buildPage(BuildContext context, GoRouterState state, ShellRouteContext shellRouteContext) { if (pageBuilder != null) { - final Widget navigator = shellRouteContext.buildNavigator( - observers: observers, restorationScopeId: restorationScopeId); + final Widget navigator = + shellRouteContext.navigatorBuilder(observers, restorationScopeId); return pageBuilder!(context, state, navigator); } return null; @@ -595,7 +603,7 @@ class ShellRoute extends ShellRouteBase { /// instead create a [StackedNavigationShell], which will manage the state /// of the StackedShellRoute. When creating this controller, a builder function /// is provided to create the container Widget for the branch Navigators. See -/// [ShellNavigatorContainerBuilder] for more details. +/// [StackedNavigationContainerBuilder] for more details. /// /// Below is a simple example of how a router configuration with /// StackedShellRoute could be achieved. In this example, a @@ -678,6 +686,7 @@ class StackedShellRoute extends ShellRouteBase { required this.branches, this.builder, this.pageBuilder, + this.navigatorContainerBuilder, this.restorationScopeId, }) : assert(branches.isNotEmpty), assert((pageBuilder != null) ^ (builder != null), @@ -726,6 +735,19 @@ class StackedShellRoute extends ShellRouteBase { /// create a custom container for the branch Navigators. final StackedShellRoutePageBuilder? pageBuilder; + /// An optional builder for a custom container for the branch Navigators. + /// + /// StackedShellRoute provides a default implementation for managing the + /// Widgets representing the branch Navigators, but in some cases a different + /// implementation may be required. When providing an implementation for this + /// builder, access is provided to a List of Widgets representing the branch + /// Navigators, where the the index corresponds to the index of in [branches]. + /// + /// The builder function is expected to return a Widget that ensures that the + /// state of the branch Widgets is maintained, for instance by inducting them + /// in the Widget tree. + final StackedNavigationContainerBuilder? navigatorContainerBuilder; + /// Representations of the different stateful route branches that this /// shell route will manage. /// @@ -740,9 +762,7 @@ class StackedShellRoute extends ShellRouteBase { Widget? buildWidget(BuildContext context, GoRouterState state, ShellRouteContext shellRouteContext) { if (builder != null) { - final StackedNavigationShell shell = - _createShell(context, state, shellRouteContext); - return builder!(context, shell.shellRouteState, shell); + return builder!(context, state, _createShell(context, shellRouteContext)); } return null; } @@ -751,9 +771,8 @@ class StackedShellRoute extends ShellRouteBase { Page? buildPage(BuildContext context, GoRouterState state, ShellRouteContext shellRouteContext) { if (pageBuilder != null) { - final StackedNavigationShell shell = - _createShell(context, state, shellRouteContext); - return pageBuilder!(context, shell.shellRouteState, shell); + return pageBuilder!( + context, state, _createShell(context, shellRouteContext)); } return null; } @@ -766,20 +785,12 @@ class StackedShellRoute extends ShellRouteBase { return branch!.navigatorKey; } - StackedNavigationShell _createShell(BuildContext context, GoRouterState state, - ShellRouteContext shellRouteContext) { - final GlobalKey navigatorKey = - navigatorKeyForSubRoute(shellRouteContext.subRoute); - final StackedShellRouteState shellRouteState = StackedShellRouteState._( - GoRouter.of(context), - this, - _shellStateKey, - state, - navigatorKey, - shellRouteContext, - ); - return StackedNavigationShell(shellRouteState: shellRouteState); - } + StackedNavigationShell _createShell( + BuildContext context, ShellRouteContext shellRouteContext) => + StackedNavigationShell( + shellRouteContext: shellRouteContext, + router: GoRouter.of(context), + containerBuilder: navigatorContainerBuilder); static List _routes(List branches) => branches.expand((StackedShellBranch e) => e.routes).toList(); @@ -858,10 +869,9 @@ class StackedShellBranch { /// The initial location for this route branch. /// /// If none is specified, the location of the first descendant [GoRoute] will - /// be used (i.e. first element in [routes], or a descendant). The default - /// location is used when loading the branch for the first time (for instance - /// when switching branch using the goBranch method in - /// [StackedShellRouteState]). + /// be used (i.e. [defaultRoute]). The initial location is used when loading + /// the branch for the first time (for instance when switching branch using + /// the goBranch method). final String? initialLocation; /// Restoration ID to save and restore the state of the navigator, including @@ -872,11 +882,21 @@ class StackedShellBranch { /// /// The observers parameter is used by the [Navigator] built for this branch. final List? observers; + + /// 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; } -/// Builder for a custom container for shell route Navigators. -typedef ShellNavigatorContainerBuilder = Widget Function(BuildContext context, - StackedShellRouteState shellRouteState, List children); +/// Builder for a custom container for the branch Navigators of a +/// [StackedShellRoute]. +typedef StackedNavigationContainerBuilder = Widget Function( + BuildContext context, + StackedNavigationShell navigationShell, + List children); /// Widget for managing the state of a [StackedShellRoute]. /// @@ -884,12 +904,13 @@ typedef ShellNavigatorContainerBuilder = Widget Function(BuildContext context, /// internally by StackedShellRoute. However, if a custom container for the /// branch Navigators is required, StackedNavigationShell can be used in /// the builder or pageBuilder methods of StackedShellRoute to facilitate this. -/// The container is created using the provided [ShellNavigatorContainerBuilder], +/// The container is created using the provided [StackedNavigationContainerBuilder], /// where the List of Widgets represent the Navigators for each branch. /// /// Example: /// ``` -/// builder: (BuildContext context, StackedShellRouteState state, Widget child) { +/// builder: (BuildContext context, GoRouterState state, +/// StackedNavigationShell navigationShell) { /// return StackedNavigationShell( /// shellRouteState: state, /// containerBuilder: (_, __, List children) => MyCustomShell(shellState: state, children: children), @@ -899,25 +920,78 @@ typedef ShellNavigatorContainerBuilder = Widget Function(BuildContext context, class StackedNavigationShell extends StatefulWidget { /// Constructs an [_StackedNavigationShell]. StackedNavigationShell({ - required this.shellRouteState, - ShellNavigatorContainerBuilder? containerBuilder, - }) : containerBuilder = containerBuilder ?? _defaultChildBuilder, - super(key: shellRouteState._shellStateKey); + required this.shellRouteContext, + required GoRouter router, + this.containerBuilder, + }) : assert(shellRouteContext.route is StackedShellRoute), + _router = router, + currentIndex = _indexOfBranchNavigatorKey( + shellRouteContext.route as StackedShellRoute, + shellRouteContext.navigatorKey), + super( + key: (shellRouteContext.route as StackedShellRoute)._shellStateKey); - static Widget _defaultChildBuilder(BuildContext context, - StackedShellRouteState shellRouteState, List children) { - return _IndexedStackedRouteBranchContainer( - currentIndex: shellRouteState.currentIndex, children: children); - } - - /// The current state of the associated [StackedShellRoute]. - final StackedShellRouteState shellRouteState; + /// The ShellRouteContext responsible for building the Navigator for the + /// current [StackedShellBranch] + final ShellRouteContext shellRouteContext; /// The builder for a custom container for shell route Navigators. - final ShellNavigatorContainerBuilder containerBuilder; + final StackedNavigationContainerBuilder? containerBuilder; + + /// The index of the currently active [StackedShellBranch]. + /// + /// Corresponds to the index in the branches field of [StackedShellRoute]. + final int currentIndex; + + final GoRouter _router; + + /// Navigate to the last location of the [StackedShellBranch] at the provided + /// index in the associated [StackedShellBranch]. + /// + /// This method will switch the currently active branch [Navigator] for the + /// [StackedShellRoute]. If the branch has not been visited before, this + /// method will navigate to initial location of the branch (see + /// [StackedShellBranch.initialLocation]). + void goBranch(int index) { + final StackedShellRoute route = + shellRouteContext.route as StackedShellRoute; + final StackedNavigationShellState? shellState = + route._shellStateKey.currentState; + if (shellState != null) { + shellState.goBranch(index); + } else { + _router.go(StackedNavigationShellState._effectiveInitialBranchLocation( + _router, route, index)); + } + } @override State createState() => StackedNavigationShellState(); + + /// Gets the state for the nearest stateful shell route in the Widget tree. + static StackedNavigationShellState of(BuildContext context) { + final StackedNavigationShellState? 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 StackedNavigationShellState? maybeOf(BuildContext context) { + final StackedNavigationShellState? shellState = + context.findAncestorStateOfType(); + return shellState; + } + + static int _indexOfBranchNavigatorKey( + StackedShellRoute route, GlobalKey navigatorKey) { + final int index = route.branches.indexWhere( + (StackedShellBranch branch) => branch.navigatorKey == navigatorKey); + assert(index >= 0); + return index; + } } /// State for StackedNavigationShell. @@ -925,14 +999,11 @@ class StackedNavigationShellState extends State with RestorationMixin { final Map _branchNavigators = {}; - StackedShellRoute get _route => widget.shellRouteState.route; - - StackedShellRouteState get _routeState => widget.shellRouteState; + StackedShellRoute get _route => + widget.shellRouteContext.route as StackedShellRoute; - GoRouter get _router => _routeState._router; + GoRouter get _router => widget._router; RouteMatcher get _matcher => _router.routeInformationParser.matcher; - GoRouteInformationProvider get _routeInformationProvider => - _router.routeInformationProvider; final Map _branchLocations = {}; @@ -949,68 +1020,93 @@ class StackedNavigationShellState extends State : identityHashCode(branch).toString(); } - _RestorableRouteMatchList _branchLocation(StackedShellBranch branch) { + _RestorableRouteMatchList _branchLocation(StackedShellBranch branch, + [bool register = true]) { return _branchLocations.putIfAbsent(branch, () { final _RestorableRouteMatchList branchLocation = _RestorableRouteMatchList(_matcher); - registerForRestoration( - branchLocation, _branchLocationRestorationScopeId(branch)); + if (register) { + registerForRestoration( + branchLocation, _branchLocationRestorationScopeId(branch)); + } return branchLocation; }); } - Widget? _navigatorForBranch(StackedShellBranch branch) { - return _branchNavigators[branch.navigatorKey]; - } - - void _setNavigatorForBranch(StackedShellBranch branch, Widget? navigator) { - navigator != null - ? _branchNavigators[branch.navigatorKey] = navigator - : _branchNavigators.remove(branch.navigatorKey); - } - RouteMatchList? _matchListForBranch(int index) => _branchLocations[_route.branches[index]]?.value; void _updateCurrentBranchStateFromWidget() { - final StackedShellBranch branch = _route.branches[_routeState.currentIndex]; - final ShellRouteContext shellRouteContext = _routeState._shellRouteContext; + final StackedShellBranch branch = _route.branches[widget.currentIndex]; + final ShellRouteContext shellRouteContext = widget.shellRouteContext; /// Create an clone of the current RouteMatchList, to prevent mutations from /// affecting the copy saved as the current state for this branch. final RouteMatchList currentBranchLocation = - shellRouteContext.routeMatchList.clone(); + shellRouteContext.routeMatchList.copy(); - final _RestorableRouteMatchList branchLocation = _branchLocation(branch); + final _RestorableRouteMatchList branchLocation = + _branchLocation(branch, false); final RouteMatchList previousBranchLocation = branchLocation.value; branchLocation.value = currentBranchLocation; - final bool hasExistingNavigator = _navigatorForBranch(branch) != null; + final bool hasExistingNavigator = + _branchNavigators[branch.navigatorKey] != null; /// Only update the Navigator of the route match list has changed final bool locationChanged = !RouteMatchList.matchListEquals( previousBranchLocation, currentBranchLocation); if (locationChanged || !hasExistingNavigator) { - final Widget currentNavigator = shellRouteContext.buildNavigator( - observers: branch.observers, - restorationScopeId: branch.restorationScopeId, - ); - _setNavigatorForBranch(branch, currentNavigator); + _branchNavigators[branch.navigatorKey] = shellRouteContext + .navigatorBuilder(branch.observers, branch.restorationScopeId); } } - void _goBranch(int index) { + Widget _indexedStackChildBuilder(BuildContext context, + StackedNavigationShell navigationShell, List children) { + return _IndexedStackedRouteBranchContainer( + currentIndex: widget.currentIndex, children: children); + } + + /// The index of the currently active [StackedShellBranch]. + /// + /// Corresponds to the index in the branches field of [StackedShellRoute]. + int get currentIndex => widget.currentIndex; + + /// Navigate to the last location of the [StackedShellBranch] at the provided + /// index in the associated [StackedShellBranch]. + /// + /// This method will switch the currently active branch [Navigator] for the + /// [StackedShellRoute]. If the branch has not been visited before, this + /// method will navigate to initial location of the branch (see + void goBranch(int index) { assert(index >= 0 && index < _route.branches.length); final RouteMatchList? matchlist = _matchListForBranch(index); if (matchlist != null && matchlist.isNotEmpty) { - _routeInformationProvider.value = matchlist.toPreParsedRouteInformation(); + final RouteInformation preParsed = + matchlist.toPreParsedRouteInformation(); + _router.go(preParsed.location!, extra: preParsed.state); + } else { + _router.go(_effectiveInitialBranchLocation(_router, _route, index)); + } + } + + static String _effectiveInitialBranchLocation( + GoRouter router, StackedShellRoute route, int index) { + final StackedShellBranch branch = route.branches[index]; + final String? initialLocation = branch.initialLocation; + if (initialLocation != null) { + return initialLocation; } else { - _router.go(_routeState._effectiveInitialBranchLocation(index)); + /// 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.locationForRoute(route)!; } } @override - void didChangeDependencies() { - super.didChangeDependencies(); + void initState() { + super.initState(); _updateCurrentBranchStateFromWidget(); } @@ -1039,27 +1135,40 @@ class StackedNavigationShellState extends State .map((StackedShellBranch branch) => _BranchNavigatorProxy( key: ObjectKey(branch), branch: branch, - navigatorForBranch: _navigatorForBranch)) + navigatorForBranch: (StackedShellBranch b) => + _branchNavigators[b.navigatorKey])) .toList(); - return widget.containerBuilder(context, _routeState, children); + + final StackedNavigationContainerBuilder containerBuilder = + widget.containerBuilder ?? _indexedStackChildBuilder; + return containerBuilder(context, widget, children); } } /// [RestorableProperty] for enabling state restoration of [RouteMatchList]s. -class _RestorableRouteMatchList extends RestorableValue { +class _RestorableRouteMatchList extends RestorableProperty { _RestorableRouteMatchList(RouteMatcher matcher) : _matchListCodec = RouteMatchListCodec(matcher); final RouteMatchListCodec _matchListCodec; - @override - RouteMatchList createDefaultValue() => RouteMatchList.empty; + RouteMatchList get value => _value; + RouteMatchList _value = RouteMatchList.empty; + set value(RouteMatchList newValue) { + if (newValue != _value) { + _value = newValue; + notifyListeners(); + } + } @override - void didUpdateValue(RouteMatchList? oldValue) { - notifyListeners(); + void initWithValue(RouteMatchList value) { + _value = value; } + @override + RouteMatchList createDefaultValue() => RouteMatchList.empty; + @override RouteMatchList fromPrimitives(Object? data) { return _matchListCodec.decodeMatchList(data) ?? RouteMatchList.empty; @@ -1144,89 +1253,3 @@ class _IndexedStackedRouteBranchContainer extends StatelessWidget { ); } } - -/// The snapshot of the current state of a [StackedShellRoute]. -/// -/// Note that this an immutable class, that represents the snapshot of the state -/// of a StackedShellRoute at a given point in time. Therefore, instances of -/// this object should not be cached, but instead passed down from the builder -/// functions of StackedShellRoute. -@immutable -class StackedShellRouteState { - /// Constructs a [_StackedShellRouteState]. - StackedShellRouteState._( - this._router, - this.route, - this._shellStateKey, - this.routerState, - GlobalKey currentNavigatorKey, - this._shellRouteContext, - ) : currentIndex = _indexOfBranchNavigatorKey(route, currentNavigatorKey); - - static int _indexOfBranchNavigatorKey( - StackedShellRoute route, GlobalKey navigatorKey) { - final int index = route.branches.indexWhere( - (StackedShellBranch branch) => branch.navigatorKey == navigatorKey); - assert(index >= 0); - return index; - } - - final GoRouter _router; - - /// The associated [StackedShellRoute] - final StackedShellRoute route; - - final GlobalKey _shellStateKey; - - /// The current route state associated with the [StackedShellRoute]. - final GoRouterState routerState; - - /// The ShellRouteContext responsible for building the Navigator for the - /// current [StackedShellBranch] - final ShellRouteContext _shellRouteContext; - - /// The index of the currently active [StackedShellBranch]. - /// - /// Corresponds to the index of the branch in the List returned from - /// branchBuilder of [StackedShellRoute]. - final int currentIndex; - - /// Navigate to the last location of the [StackedShellBranch] at the provided - /// index in the associated [StackedShellBranch]. - /// - /// This method will switch the currently active branch [Navigator] for the - /// [StackedShellRoute]. If the branch has not been visited before, this - /// method will navigate to initial location of the branch (see - /// [StackedShellBranch.initialLocation]). - void goBranch({required int index}) { - final StackedNavigationShellState? shellState = _shellStateKey.currentState; - if (shellState != null) { - shellState._goBranch(index); - } else { - assert(_router != null); - _router.go(_effectiveInitialBranchLocation(index)); - } - } - - String _effectiveInitialBranchLocation(int index) { - return _router.routeInformationParser.configuration - .effectiveInitialBranchLocation(route.branches[index]); - } - - /// Gets the state for the nearest stateful shell route in the Widget tree. - static StackedShellRouteState of(BuildContext context) { - final StackedNavigationShellState? shellState = - context.findAncestorStateOfType(); - assert(shellState != null); - return shellState!._routeState; - } - - /// Gets the state for the nearest stateful shell route in the Widget tree. - /// - /// Returns null if no stateful shell route is found. - static StackedShellRouteState? maybeOf(BuildContext context) { - final StackedNavigationShellState? shellState = - context.findAncestorStateOfType(); - return shellState?._routeState; - } -} diff --git a/packages/go_router/lib/src/router.dart b/packages/go_router/lib/src/router.dart index 14b8ecf62a91..c0edb8de0d2d 100644 --- a/packages/go_router/lib/src/router.dart +++ b/packages/go_router/lib/src/router.dart @@ -183,6 +183,13 @@ class GoRouter extends ChangeNotifier implements RouterConfig { queryParams: queryParams, ); + /// Get the location for the provided route. + /// + /// Builds the absolute path for the route, by concatenating the paths of the + /// route and all its ancestors. + String? locationForRoute(RouteBase route) => + _routeInformationParser.configuration.locationForRoute(route); + /// Navigate to a URI location w/ optional query parameters, e.g. /// `/family/f2/person/p1?color=blue` void go(String location, {Object? extra}) { diff --git a/packages/go_router/lib/src/typedefs.dart b/packages/go_router/lib/src/typedefs.dart index aeeca23554ba..9b214957d23b 100644 --- a/packages/go_router/lib/src/typedefs.dart +++ b/packages/go_router/lib/src/typedefs.dart @@ -37,15 +37,15 @@ typedef ShellRoutePageBuilder = Page Function( /// The widget builder for [StackedShellRoute]. typedef StackedShellRouteBuilder = Widget Function( BuildContext context, - StackedShellRouteState state, - Widget child, + GoRouterState state, + StackedNavigationShell navigationShell, ); /// The page builder for [StackedShellRoute]. typedef StackedShellRoutePageBuilder = Page Function( BuildContext context, - StackedShellRouteState state, - Widget child, + GoRouterState state, + StackedNavigationShell navigationShell, ); /// Signature of a go router builder function with navigator. diff --git a/packages/go_router/test/builder_test.dart b/packages/go_router/test/builder_test.dart index 9686aad9778a..7ed7d0c88558 100644 --- a/packages/go_router/test/builder_test.dart +++ b/packages/go_router/test/builder_test.dart @@ -337,9 +337,9 @@ void main() { routes: [ StackedShellRoute( restorationScopeId: 'shell', - builder: (BuildContext context, StackedShellRouteState state, - Widget child) => - _HomeScreen(child: child), + builder: (BuildContext context, GoRouterState state, + StackedNavigationShell navigationShell) => + _HomeScreen(child: navigationShell), branches: [ StackedShellBranch( navigatorKey: shellNavigatorKey, @@ -445,7 +445,6 @@ class _BuilderTestWidget extends StatelessWidget { return MaterialApp( home: builder.tryBuild(context, matches, false, routeConfiguration.navigatorKey, , GoRouterState>{}), - // builder: (context, child) => , ); } } diff --git a/packages/go_router/test/configuration_test.dart b/packages/go_router/test/configuration_test.dart index db7d202afbba..f7ff546e1d1d 100644 --- a/packages/go_router/test/configuration_test.dart +++ b/packages/go_router/test/configuration_test.dart @@ -546,10 +546,14 @@ void main() { }, ); - expect('/a', config.findStackedShellBranchDefaultLocation(branchA)); - expect( - '/a/x/y1', config.findStackedShellBranchDefaultLocation(branchY)); - expect('/b1', config.findStackedShellBranchDefaultLocation(branchB)); + String? initialLocation(StackedShellBranch branch) { + final GoRoute? route = branch.defaultRoute; + return route != null ? config.locationForRoute(route) : null; + } + + expect('/a', initialLocation(branchA)); + expect('/a/x/y1', initialLocation(branchY)); + expect('/b1', initialLocation(branchB)); }, ); diff --git a/packages/go_router/test/go_router_test.dart b/packages/go_router/test/go_router_test.dart index 1207222449b2..0163085785de 100644 --- a/packages/go_router/test/go_router_test.dart +++ b/packages/go_router/test/go_router_test.dart @@ -2501,13 +2501,13 @@ void main() { testWidgets('StackedShellRoute supports nested routes with params', (WidgetTester tester) async { - StackedShellRouteState? routeState; + StackedNavigationShell? routeState; final List routes = [ StackedShellRoute( - builder: (BuildContext context, StackedShellRouteState state, - Widget child) { - routeState = state; - return child; + builder: (BuildContext context, GoRouterState state, + StackedNavigationShell navigationShell) { + routeState = navigationShell; + return navigationShell; }, branches: [ StackedShellBranch( @@ -2566,12 +2566,12 @@ void main() { expect(matches.pathParameters['fid'], fid); expect(matches.pathParameters['pid'], pid); - routeState?.goBranch(index: 0); + routeState?.goBranch(0); await tester.pumpAndSettle(); expect(find.text('Screen A'), findsOneWidget); expect(find.byType(PersonScreen), findsNothing); - routeState?.goBranch(index: 1); + routeState?.goBranch(1); await tester.pumpAndSettle(); expect(find.text('Screen A'), findsNothing); expect(find.byType(PersonScreen), findsOneWidget); @@ -3076,9 +3076,9 @@ void main() { final List routes = [ StackedShellRoute( - builder: (BuildContext context, StackedShellRouteState state, - Widget child) => - child, + builder: (BuildContext context, GoRouterState state, + StackedNavigationShell navigationShell) => + navigationShell, branches: [ StackedShellBranch(routes: [ GoRoute( @@ -3121,9 +3121,9 @@ void main() { const Text('Root'), routes: [ StackedShellRoute( - builder: (BuildContext context, StackedShellRouteState state, - Widget child) => - child, + builder: (BuildContext context, GoRouterState state, + StackedNavigationShell navigationShell) => + navigationShell, branches: [ StackedShellBranch(routes: [ GoRoute( @@ -3163,14 +3163,14 @@ void main() { GlobalKey(); final GlobalKey statefulWidgetKey = GlobalKey(); - StackedShellRouteState? routeState; + StackedNavigationShell? routeState; final List routes = [ StackedShellRoute( - builder: (BuildContext context, StackedShellRouteState state, - Widget child) { - routeState = state; - return child; + builder: (BuildContext context, GoRouterState state, + StackedNavigationShell navigationShell) { + routeState = navigationShell; + return navigationShell; }, branches: [ StackedShellBranch( @@ -3221,21 +3221,21 @@ void main() { expect(find.text('Screen C'), findsNothing); expect(find.text('Screen D'), findsNothing); - routeState!.goBranch(index: 1); + routeState!.goBranch(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(index: 2); + routeState!.goBranch(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(index: 3); + routeState!.goBranch(3); await tester.pumpAndSettle(); expect(find.text('Screen A'), findsNothing); expect(find.text('Screen B'), findsNothing); @@ -3244,7 +3244,7 @@ void main() { expect(() { // Verify that navigation to unknown index fails - routeState!.goBranch(index: 4); + routeState!.goBranch(4); }, throwsA(isA())); }); @@ -3255,14 +3255,14 @@ void main() { GlobalKey(); final GlobalKey statefulWidgetKey = GlobalKey(); - StackedShellRouteState? routeState; + StackedNavigationShell? routeState; final List routes = [ StackedShellRoute( - builder: (BuildContext context, StackedShellRouteState state, - Widget child) { - routeState = state; - return child; + builder: (BuildContext context, GoRouterState state, + StackedNavigationShell navigationShell) { + routeState = navigationShell; + return navigationShell; }, branches: [ StackedShellBranch(routes: [ @@ -3300,13 +3300,13 @@ void main() { expect(find.text('Screen A Detail'), findsOneWidget); expect(find.text('Screen B'), findsNothing); - routeState!.goBranch(index: 1); + routeState!.goBranch(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(index: 0); + routeState!.goBranch(0); await tester.pumpAndSettle(); expect(statefulWidgetKey.currentState?.counter, equals(1)); @@ -3325,23 +3325,23 @@ void main() { GlobalKey(); final GlobalKey statefulWidgetKey = GlobalKey(); - StackedShellRouteState? routeState1; - StackedShellRouteState? routeState2; + StackedNavigationShell? routeState1; + StackedNavigationShell? routeState2; final List routes = [ StackedShellRoute( - builder: (BuildContext context, StackedShellRouteState state, - Widget child) { - routeState1 = state; - return child; + builder: (BuildContext context, GoRouterState state, + StackedNavigationShell navigationShell) { + routeState1 = navigationShell; + return navigationShell; }, branches: [ StackedShellBranch(routes: [ StackedShellRoute( - builder: (BuildContext context, StackedShellRouteState state, - Widget child) { - routeState2 = state; - return child; + builder: (BuildContext context, GoRouterState state, + StackedNavigationShell navigationShell) { + routeState2 = navigationShell; + return navigationShell; }, branches: [ StackedShellBranch(routes: [ @@ -3393,23 +3393,23 @@ void main() { initialLocation: '/a/detailA', navigatorKey: rootNavigatorKey); statefulWidgetKey.currentState?.increment(); expect(find.text('Screen A Detail'), findsOneWidget); - routeState2!.goBranch(index: 1); + routeState2!.goBranch(1); await tester.pumpAndSettle(); expect(find.text('Screen B'), findsOneWidget); - routeState1!.goBranch(index: 1); + routeState1!.goBranch(1); await tester.pumpAndSettle(); expect(find.text('Screen D'), findsOneWidget); - routeState1!.goBranch(index: 0); + routeState1!.goBranch(0); await tester.pumpAndSettle(); expect(find.text('Screen B'), findsOneWidget); - routeState2!.goBranch(index: 2); + routeState2!.goBranch(2); await tester.pumpAndSettle(); expect(find.text('Screen C'), findsOneWidget); - routeState2!.goBranch(index: 0); + routeState2!.goBranch(0); await tester.pumpAndSettle(); expect(find.text('Screen A Detail'), findsOneWidget); expect(statefulWidgetKey.currentState?.counter, equals(1)); @@ -3424,14 +3424,14 @@ void main() { GlobalKey(); final GlobalKey sectionBNavigatorKey = GlobalKey(); - StackedShellRouteState? routeState; + StackedNavigationShell? routeState; final List routes = [ StackedShellRoute( - builder: (BuildContext context, StackedShellRouteState state, - Widget child) { - routeState = state; - return child; + builder: (BuildContext context, GoRouterState state, + StackedNavigationShell navigationShell) { + routeState = navigationShell; + return navigationShell; }, branches: [ StackedShellBranch( @@ -3493,7 +3493,7 @@ void main() { expect(find.text('Screen B'), findsOneWidget); expect(find.text('Screen B Detail'), findsNothing); - routeState!.goBranch(index: 0); + routeState!.goBranch(0); await tester.pumpAndSettle(); expect(find.text('Screen A'), findsNothing); expect(find.text('Screen A Detail'), findsOneWidget); @@ -3509,14 +3509,14 @@ void main() { 'between branches in StackedShellRoute', (WidgetTester tester) async { final GlobalKey rootNavigatorKey = GlobalKey(); - StackedShellRouteState? routeState; + StackedNavigationShell? routeState; final List routes = [ StackedShellRoute( - builder: (BuildContext context, StackedShellRouteState state, - Widget child) { - routeState = state; - return child; + builder: (BuildContext context, GoRouterState state, + StackedNavigationShell navigationShell) { + routeState = navigationShell; + return navigationShell; }, branches: [ StackedShellBranch(routes: [ @@ -3546,12 +3546,12 @@ void main() { expect(find.text('Screen A'), findsNothing); expect(find.text('Screen B - X'), findsOneWidget); - routeState!.goBranch(index: 0); + routeState!.goBranch(0); await tester.pumpAndSettle(); expect(find.text('Screen A'), findsOneWidget); expect(find.text('Screen B - X'), findsNothing); - routeState!.goBranch(index: 1); + routeState!.goBranch(1); await tester.pumpAndSettle(); expect(find.text('Screen A'), findsNothing); expect(find.text('Screen B - X'), findsOneWidget); @@ -3563,7 +3563,7 @@ void main() { (WidgetTester tester) async { final GlobalKey rootNavigatorKey = GlobalKey(); - StackedShellRouteState? routeState; + StackedNavigationShell? routeState; final List routes = [ GoRoute( @@ -3572,10 +3572,10 @@ void main() { Text('Common - ${state.extra}'), ), StackedShellRoute( - builder: (BuildContext context, StackedShellRouteState state, - Widget child) { - routeState = state; - return child; + builder: (BuildContext context, GoRouterState state, + StackedNavigationShell navigationShell) { + routeState = navigationShell; + return navigationShell; }, branches: [ StackedShellBranch(routes: [ @@ -3611,11 +3611,11 @@ void main() { expect(find.text('Screen B'), findsNothing); expect(find.text('Common - X'), findsOneWidget); - routeState!.goBranch(index: 0); + routeState!.goBranch(0); await tester.pumpAndSettle(); expect(find.text('Screen A'), findsOneWidget); - routeState!.goBranch(index: 1); + routeState!.goBranch(1); await tester.pumpAndSettle(); expect(find.text('Screen A'), findsNothing); expect(find.text('Screen B'), findsNothing); @@ -3627,14 +3627,14 @@ void main() { 'StackedShellRoute', (WidgetTester tester) async { final GlobalKey rootNavigatorKey = GlobalKey(); - StackedShellRouteState? routeState; + StackedNavigationShell? routeState; final List routes = [ StackedShellRoute( - builder: (BuildContext context, StackedShellRouteState state, - Widget child) { - routeState = state; - return child; + builder: (BuildContext context, GoRouterState state, + StackedNavigationShell navigationShell) { + routeState = navigationShell; + return navigationShell; }, branches: [ StackedShellBranch(routes: [ @@ -3699,19 +3699,19 @@ void main() { expect(find.text('Screen A'), findsOneWidget); expect(find.text('Screen B Detail'), findsNothing); - routeState!.goBranch(index: 1); + routeState!.goBranch(1); await tester.pumpAndSettle(); expect(find.text('Screen A'), findsNothing); expect(find.text('Screen B Detail1'), findsOneWidget); - routeState!.goBranch(index: 2); + routeState!.goBranch(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(index: 1); + routeState!.goBranch(1); await tester.pumpAndSettle(); expect(find.text('Screen A'), findsNothing); expect(find.text('Screen B Detail2'), findsOneWidget); @@ -4342,16 +4342,16 @@ void main() { GlobalKey(); final GlobalKey statefulWidgetKeyC = GlobalKey(); - StackedShellRouteState? routeState; + StackedNavigationShell? routeState; final List routes = [ StackedShellRoute( restorationScopeId: 'shell', - pageBuilder: (BuildContext context, StackedShellRouteState state, - Widget child) { - routeState = state; + pageBuilder: (BuildContext context, GoRouterState state, + StackedNavigationShell navigationShell) { + routeState = navigationShell; return MaterialPage( - restorationId: 'shellWidget', child: child); + restorationId: 'shellWidget', child: navigationShell); }, branches: [ StackedShellBranch(restorationScopeId: 'branchA', routes: [ @@ -4436,7 +4436,7 @@ void main() { statefulWidgetKeyC.currentState?.increment(); expect(statefulWidgetKeyC.currentState?.counter, equals(1)); - routeState!.goBranch(index: 0); + routeState!.goBranch(0); await tester.pumpAndSettle(); expect(find.text('Screen A Detail'), findsOneWidget); @@ -4446,12 +4446,12 @@ void main() { expect(find.text('Screen A Detail'), findsOneWidget); expect(statefulWidgetKeyA.currentState?.counter, equals(1)); - routeState!.goBranch(index: 1); + routeState!.goBranch(1); await tester.pumpAndSettle(); expect(find.text('Screen B Detail'), findsOneWidget); expect(statefulWidgetKeyB.currentState?.counter, equals(1)); - routeState!.goBranch(index: 2); + routeState!.goBranch(2); await tester.pumpAndSettle(); expect(find.text('Screen C Detail'), findsOneWidget); // State of branch C should not have been restored @@ -4467,17 +4467,17 @@ void main() { GlobalKey(); final GlobalKey statefulWidgetKeyB = GlobalKey(); - StackedShellRouteState? routeStateRoot; - StackedShellRouteState? routeStateNested; + StackedNavigationShell? routeStateRoot; + StackedNavigationShell? routeStateNested; final List routes = [ StackedShellRoute( restorationScopeId: 'shell', - pageBuilder: (BuildContext context, StackedShellRouteState state, - Widget child) { - routeStateRoot = state; + pageBuilder: (BuildContext context, GoRouterState state, + StackedNavigationShell navigationShell) { + routeStateRoot = navigationShell; return MaterialPage( - restorationId: 'shellWidget', child: child); + restorationId: 'shellWidget', child: navigationShell); }, branches: [ StackedShellBranch(restorationScopeId: 'branchA', routes: [ @@ -4505,11 +4505,12 @@ void main() { routes: [ StackedShellRoute( restorationScopeId: 'branchB-nested-shell', - pageBuilder: (BuildContext context, - StackedShellRouteState state, Widget child) { - routeStateNested = state; + pageBuilder: (BuildContext context, GoRouterState state, + StackedNavigationShell navigationShell) { + routeStateNested = navigationShell; return MaterialPage( - restorationId: 'shellWidget-nested', child: child); + restorationId: 'shellWidget-nested', + child: navigationShell); }, branches: [ StackedShellBranch( @@ -4534,17 +4535,6 @@ void main() { ), ], ), - // GoRoute( - // path: '/bPushed', - // pageBuilder: createPageBuilder( - // restorationId: 'screenBDetail', - // child: Column(children: [ - // const Text('Screen B Pushed Detail'), - // DummyRestorableStatefulWidget( - // key: statefulWidgetKeyB, - // restorationId: 'counterB'), - // ])), - // ), ]), StackedShellBranch( restorationScopeId: 'branchC-nested', @@ -4570,16 +4560,15 @@ void main() { statefulWidgetKeyA.currentState?.increment(); expect(statefulWidgetKeyA.currentState?.counter, equals(1)); - routeStateRoot!.goBranch(index: 1); + routeStateRoot!.goBranch(1); await tester.pumpAndSettle(); - // router.push('/bPushed'); router.go('/b/detailB'); await tester.pumpAndSettle(); statefulWidgetKeyB.currentState?.increment(); expect(statefulWidgetKeyB.currentState?.counter, equals(1)); - routeStateRoot!.goBranch(index: 0); + routeStateRoot!.goBranch(0); await tester.pumpAndSettle(); expect(find.text('Screen A Detail'), findsOneWidget); expect(find.text('Screen B'), findsNothing); @@ -4593,26 +4582,20 @@ void main() { expect(find.text('Screen B Pushed Detail'), findsNothing); expect(statefulWidgetKeyA.currentState?.counter, equals(1)); - routeStateRoot!.goBranch(index: 1); + routeStateRoot!.goBranch(1); await tester.pumpAndSettle(); expect(find.text('Screen A Detail'), findsNothing); expect(find.text('Screen B'), findsNothing); - //expect(find.text('Screen B Pushed Detail'), findsOneWidget); expect(find.text('Screen B Detail'), findsOneWidget); expect(statefulWidgetKeyB.currentState?.counter, equals(1)); - routeStateNested!.goBranch(index: 1); + routeStateNested!.goBranch(1); await tester.pumpAndSettle(); - routeStateNested!.goBranch(index: 0); + routeStateNested!.goBranch(0); await tester.pumpAndSettle(); expect(find.text('Screen B Detail'), findsOneWidget); expect(statefulWidgetKeyB.currentState?.counter, equals(1)); - - // router.pop(); - // await tester.pumpAndSettle(); - // expect(find.text('Screen B'), findsOneWidget); - // expect(find.text('Screen B Pushed Detail'), findsNothing); }); }); } diff --git a/packages/go_router/test/match_test.dart b/packages/go_router/test/match_test.dart index 22937c7849fb..36710403ee71 100644 --- a/packages/go_router/test/match_test.dart +++ b/packages/go_router/test/match_test.dart @@ -160,7 +160,7 @@ void main() { const {}, ); - final RouteMatchList list2 = list.clone(); + final RouteMatchList list2 = list.copy(); expect(RouteMatchList.matchListEquals(list, list2), true); }); diff --git a/packages/go_router/test/test_helpers.dart b/packages/go_router/test/test_helpers.dart index 8e1e8cde36b7..a21d634a4eb1 100644 --- a/packages/go_router/test/test_helpers.dart +++ b/packages/go_router/test/test_helpers.dart @@ -292,9 +292,9 @@ GoRouterPageBuilder createPageBuilder( (BuildContext context, GoRouterState state) => MaterialPage(restorationId: restorationId, child: child); -StackedShellRouteBuilder mockStackedShellBuilder = - (BuildContext context, StackedShellRouteState state, Widget child) { - return child; +StackedShellRouteBuilder mockStackedShellBuilder = (BuildContext context, + GoRouterState state, StackedNavigationShell navigationShell) { + return navigationShell; }; RouteMatch createRouteMatch(RouteBase route, String location) { From 6f35636a3476ac18ae8c81e525d539c2aa3af5e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20Lo=CC=88fstrand?= Date: Thu, 20 Apr 2023 17:26:04 +0200 Subject: [PATCH 099/112] Doc updates - removed/replaced references to StackedShellRouteState. --- packages/go_router/lib/src/route.dart | 57 ++++++++++++--------------- 1 file changed, 25 insertions(+), 32 deletions(-) diff --git a/packages/go_router/lib/src/route.dart b/packages/go_router/lib/src/route.dart index 8649dce253b9..26c0b584cd72 100644 --- a/packages/go_router/lib/src/route.dart +++ b/packages/go_router/lib/src/route.dart @@ -579,31 +579,28 @@ class ShellRoute extends ShellRouteBase { /// /// Like [ShellRoute], either a [builder] or a [pageBuilder] must be provided /// when creating a StackedShellRoute. However, these builders differ slightly -/// in that they accept a [StackedShellRouteState] parameter instead of a -/// GoRouterState. The StackedShellRouteState can be used to access information +/// in that they accept a [StackedNavigationShell] parameter instead of a +/// child Widget. The StackedNavigationShell 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 [StackedShellRouteState.goBranch], for +/// accomplished by using the method [StackedNavigationShell.goBranch], for /// example: /// /// ``` -/// void _onItemTapped(StackedShellRouteState shellState, int index) { -/// shellState.goBranch(index: index); +/// void _onItemTapped(int index) { +/// navigationShell.goBranch(index: index); /// } /// ``` /// -/// The final child parameter of the builders is a container Widget that manages -/// and maintains the state of the branch Navigators. Typically, a shell is -/// built around this Widget, for example by using it as the body of [Scaffold] -/// with a [BottomNavigationBar]. +/// The StackedNavigationShell is also responsible for managing and maintaining +/// the state of the branch Navigators. Typically, a shell is built around this +/// Widget, for example by using it as the body of [Scaffold] with a +/// [BottomNavigationBar]. /// /// Sometimes greater control is needed over the layout and animations of the /// Widgets representing the branch Navigators. In such cases, a custom -/// implementation can choose to ignore the child parameter of the builders and -/// instead create a [StackedNavigationShell], which will manage the state -/// of the StackedShellRoute. When creating this controller, a builder function -/// is provided to create the container Widget for the branch Navigators. See -/// [StackedNavigationContainerBuilder] for more details. +/// implementation can choose to provide a [navigatorContainerBuilder], in +/// which a custom container Widget can be provided for the branch Navigators. /// /// Below is a simple example of how a router configuration with /// StackedShellRoute could be achieved. In this example, a @@ -622,9 +619,9 @@ class ShellRoute extends ShellRouteBase { /// initialLocation: '/a', /// routes: [ /// StackedShellRoute( -/// builder: (BuildContext context, StackedShellRouteState state, -/// Widget child) { -/// return ScaffoldWithNavBar(shellState: state, body: child); +/// builder: (BuildContext context, GoRouterState state, +/// StackedNavigationShell navigationShell) { +/// return ScaffoldWithNavBar(navigationShell: navigationShell); /// }, /// branches: [ /// /// The first branch, i.e. tab 'A' @@ -703,15 +700,13 @@ class StackedShellRoute extends ShellRouteBase { /// The widget builder for a stateful shell route. /// - /// Similar to [GoRoute.builder], but with an additional child parameter. This - /// child parameter is the Widget managing the nested navigation for the + /// Similar to [GoRoute.builder], but with an additional + /// [StackedNavigationShell] parameter. StackedNavigationShell is a Widget + /// responsible for managing the nested navigation for the /// matching sub-routes. Typically, a shell route builds its shell around this - /// Widget. - /// - /// Instead of a GoRouterState, this builder function accepts a - /// [StackedShellRouteState] object, which can be used to access information + /// Widget. StackedNavigationShell can also be used to access information /// about which branch is active, and also to navigate to a different branch - /// (using [StackedShellRouteState.goBranch]). + /// (using [StackedNavigationShell.goBranch]). /// /// Custom implementations may choose to ignore the child parameter passed to /// the builder function, and instead use [StackedNavigationShell] to @@ -720,15 +715,13 @@ class StackedShellRoute extends ShellRouteBase { /// The page builder for a stateful shell route. /// - /// Similar to [GoRoute.pageBuilder], but with an additional child parameter. - /// This child parameter is the Widget managing the nested navigation for the + /// Similar to [GoRoute.pageBuilder], but with an additional + /// [StackedNavigationShell] parameter. StackedNavigationShell is a Widget + /// responsible for managing the nested navigation for the /// matching sub-routes. Typically, a shell route builds its shell around this - /// Widget. - /// - /// Instead of a GoRouterState, this builder function accepts a - /// [StackedShellRouteState] object, which can be used to access information + /// Widget. StackedNavigationShell can also be used to access information /// about which branch is active, and also to navigate to a different branch - /// (using [StackedShellRouteState.goBranch]). + /// (using [StackedNavigationShell.goBranch]). /// /// Custom implementations may choose to ignore the child parameter passed to /// the builder function, and instead use [StackedNavigationShell] to @@ -836,7 +829,7 @@ class StackedShellRoute 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 [StackedShellRouteState]). +/// using the goBranch method in [StackedNavigationShell]). /// /// A separate [Navigator] will be built for each StackedShellBranch in a /// [StackedShellRoute], and the routes of this branch will be placed onto that From 16c095c1016e886f73d6321c2fdd53cac965017b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20Lo=CC=88fstrand?= Date: Sat, 29 Apr 2023 02:46:27 +0200 Subject: [PATCH 100/112] Refactoring due to review feedback. Re-introduced StatefulShellRoute as a base class for StackedShellRoute. Made effectiveInitialBranchLocation public. Removed _GoRouterDelegateRestorableProperties and _RestorablePushCount. --- packages/go_router/CHANGELOG.md | 4 +- packages/go_router/example/README.md | 2 +- .../stacked_shell_state_restoration.dart | 34 +- ...l_route.dart => stateful_shell_route.dart} | 54 +-- packages/go_router/lib/go_router.dart | 15 +- packages/go_router/lib/src/builder.dart | 96 ++--- packages/go_router/lib/src/configuration.dart | 88 +---- packages/go_router/lib/src/delegate.dart | 96 +---- packages/go_router/lib/src/route.dart | 346 ++++++++++-------- packages/go_router/lib/src/typedefs.dart | 12 +- packages/go_router/test/builder_test.dart | 10 +- .../go_router/test/configuration_test.dart | 76 ++-- packages/go_router/test/delegate_test.dart | 20 +- packages/go_router/test/go_router_test.dart | 336 +++++++---------- packages/go_router/test/test_helpers.dart | 4 +- 15 files changed, 518 insertions(+), 675 deletions(-) rename packages/go_router/example/lib/{stacked_shell_route.dart => stateful_shell_route.dart} (91%) diff --git a/packages/go_router/CHANGELOG.md b/packages/go_router/CHANGELOG.md index 5ae297b14541..fbb05e7e19cb 100644 --- a/packages/go_router/CHANGELOG.md +++ b/packages/go_router/CHANGELOG.md @@ -1,11 +1,11 @@ ## 6.6.0 -- Introduces `StackedShellRoute` to support using separate +- Introduces `StatefulShellRoute` to support using separate navigators for child routes as well as preserving state in each navigation tree (flutter/flutter#99124). - Updates documentation for `pageBuilder` and `builder` fields of `ShellRoute`, to more correctly describe the meaning of the child argument in the builder functions. -- Adds support for restorationId to ShellRoute (and StackedShellRoute). +- Adds support for restorationId to ShellRoute (and StatefulShellRoute). - Adds support for restoring imperatively pushed routes. ## 6.5.9 diff --git a/packages/go_router/example/README.md b/packages/go_router/example/README.md index eeb9d252ce52..5f2a9957bc75 100644 --- a/packages/go_router/example/README.md +++ b/packages/go_router/example/README.md @@ -33,7 +33,7 @@ An example to demonstrate how to use handle a sign-in flow with a stream authent ## [Stateful Nested Navigation](https://github.com/flutter/packages/blob/main/packages/go_router/example/lib/stacked_shell_route.dart) `flutter run lib/stacked_shell_route.dart` -An example to demonstrate how to use a `StackedShellRoute` to create stateful nested navigation, with a +An example to demonstrate how to use a `StatefulShellRoute` to create stateful nested navigation, with a `BottomNavigationBar`. ## [Books app](https://github.com/flutter/packages/blob/main/packages/go_router/example/lib/books) diff --git a/packages/go_router/example/lib/others/stacked_shell_state_restoration.dart b/packages/go_router/example/lib/others/stacked_shell_state_restoration.dart index 4a246f655d0a..5d7abe19a9fd 100644 --- a/packages/go_router/example/lib/others/stacked_shell_state_restoration.dart +++ b/packages/go_router/example/lib/others/stacked_shell_state_restoration.dart @@ -11,13 +11,13 @@ final GlobalKey _rootNavigatorKey = final GlobalKey _tabANavigatorKey = GlobalKey(debugLabel: 'tabANav'); -void main() => runApp(RestorableStackedShellRouteExampleApp()); +void main() => runApp(RestorableStatefulShellRouteExampleApp()); -/// An example demonstrating how to use StackedShellRoute with state +/// An example demonstrating how to use StatefulShellRoute with state /// restoration. -class RestorableStackedShellRouteExampleApp extends StatelessWidget { +class RestorableStatefulShellRouteExampleApp extends StatelessWidget { /// Creates a NestedTabNavigationExampleApp - RestorableStackedShellRouteExampleApp({super.key}); + RestorableStatefulShellRouteExampleApp({super.key}); final GoRouter _router = GoRouter( navigatorKey: _rootNavigatorKey, @@ -26,9 +26,9 @@ class RestorableStackedShellRouteExampleApp extends StatelessWidget { routes: [ StackedShellRoute( restorationScopeId: 'shell1', - branches: [ + branches: [ /// The route branch for the first tab of the bottom navigation bar. - StackedShellBranch( + StatefulShellBranch( navigatorKey: _tabANavigatorKey, restorationScopeId: 'branchA', routes: [ @@ -59,16 +59,16 @@ class RestorableStackedShellRouteExampleApp extends StatelessWidget { ), /// The route branch for the third tab of the bottom navigation bar. - StackedShellBranch( + StatefulShellBranch( restorationScopeId: 'branchB', routes: [ - StackedShellRoute( + StatefulShellRoute( restorationScopeId: 'shell2', /// This bottom tab uses a nested shell, wrapping sub routes in a /// top TabBar. - branches: [ - StackedShellBranch( + branches: [ + StatefulShellBranch( restorationScopeId: 'branchB1', routes: [ GoRoute( @@ -96,7 +96,7 @@ class RestorableStackedShellRouteExampleApp extends StatelessWidget { ], ), ]), - StackedShellBranch( + StatefulShellBranch( restorationScopeId: 'branchB2', routes: [ GoRoute( @@ -126,12 +126,12 @@ class RestorableStackedShellRouteExampleApp extends StatelessWidget { ]), ], pageBuilder: (BuildContext context, GoRouterState state, - StackedNavigationShell navigationShell) { + StatefulNavigationShell navigationShell) { return MaterialPage( restorationId: 'shellWidget2', child: navigationShell); }, navigatorContainerBuilder: (BuildContext context, - StackedNavigationShell navigationShell, + StatefulNavigationShell navigationShell, List children) => /// Returning a customized container for the branch @@ -146,7 +146,7 @@ class RestorableStackedShellRouteExampleApp extends StatelessWidget { ), ], pageBuilder: (BuildContext context, GoRouterState state, - StackedNavigationShell navigationShell) { + StatefulNavigationShell navigationShell) { return MaterialPage( restorationId: 'shellWidget1', child: ScaffoldWithNavBar(navigationShell: navigationShell)); @@ -178,7 +178,7 @@ class ScaffoldWithNavBar extends StatelessWidget { }) : super(key: key ?? const ValueKey('ScaffoldWithNavBar')); /// The navigation shell and container for the branch Navigators. - final StackedNavigationShell navigationShell; + final StatefulNavigationShell navigationShell; @override Widget build(BuildContext context) { @@ -345,8 +345,8 @@ class TabbedRootScreen extends StatefulWidget { const TabbedRootScreen( {required this.navigationShell, required this.children, super.key}); - /// The current state of the parent StackedShellRoute. - final StackedNavigationShell navigationShell; + /// The current state of the parent StatefulShellRoute. + final StatefulNavigationShell navigationShell; /// The children (Navigators) to display in the [TabBarView]. final List children; diff --git a/packages/go_router/example/lib/stacked_shell_route.dart b/packages/go_router/example/lib/stateful_shell_route.dart similarity index 91% rename from packages/go_router/example/lib/stacked_shell_route.dart rename to packages/go_router/example/lib/stateful_shell_route.dart index d50c6a4f6578..58ae8129abd9 100644 --- a/packages/go_router/example/lib/stacked_shell_route.dart +++ b/packages/go_router/example/lib/stateful_shell_route.dart @@ -16,7 +16,7 @@ final GlobalKey _tabANavigatorKey = // navigation state is maintained separately for each tab. This setup also // enables deep linking into nested pages. // -// This example demonstrates how to display routes within a StackedShellRoute, +// This example demonstrates how to display routes within a StatefulShellRoute, // that are places on separate navigators. The example also demonstrates how // state is maintained when switching between different tabs (and thus branches // and Navigators). @@ -41,9 +41,9 @@ class NestedTabNavigationExampleApp extends StatelessWidget { const ModalScreen(), ), StackedShellRoute( - branches: [ + branches: [ /// The route branch for the first tab of the bottom navigation bar. - StackedShellBranch( + StatefulShellBranch( navigatorKey: _tabANavigatorKey, routes: [ GoRoute( @@ -67,7 +67,7 @@ class NestedTabNavigationExampleApp extends StatelessWidget { ), /// The route branch for the second tab of the bottom navigation bar. - StackedShellBranch( + StatefulShellBranch( /// It's not necessary to provide a navigatorKey if it isn't also /// needed elsewhere. If not provided, a default key will be used. // navigatorKey: _tabBNavigatorKey, @@ -98,18 +98,18 @@ class NestedTabNavigationExampleApp extends StatelessWidget { ), /// The route branch for the third tab of the bottom navigation bar. - StackedShellBranch( - /// StackedShellBranch will automatically use the first descendant + StatefulShellBranch( + /// 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', routes: [ - StackedShellRoute( + StatefulShellRoute( /// This bottom tab uses a nested shell, wrapping sub routes in a /// top TabBar. - branches: [ - StackedShellBranch(routes: [ + branches: [ + StatefulShellBranch(routes: [ GoRoute( path: '/c1', builder: (BuildContext context, GoRouterState state) => @@ -129,7 +129,7 @@ class NestedTabNavigationExampleApp extends StatelessWidget { ], ), ]), - StackedShellBranch(routes: [ + StatefulShellBranch(routes: [ GoRoute( path: '/c2', builder: (BuildContext context, GoRouterState state) => @@ -151,8 +151,8 @@ class NestedTabNavigationExampleApp extends StatelessWidget { ]), ], builder: (BuildContext context, GoRouterState state, - StackedNavigationShell navigationShell) { - /// This nested StackedShellRoute demonstrates the use of a + StatefulNavigationShell navigationShell) { + /// This nested StatefulShellRoute demonstrates the use of a /// custom container (TabBarView) for the branch Navigators. /// In this implementation, no customization is done in the /// builder function (navigationShell itself is simply used as @@ -162,7 +162,7 @@ class NestedTabNavigationExampleApp extends StatelessWidget { return navigationShell; }, navigatorContainerBuilder: (BuildContext context, - StackedNavigationShell navigationShell, + StatefulNavigationShell navigationShell, List children) => /// Returning a customized container for the branch @@ -177,18 +177,18 @@ class NestedTabNavigationExampleApp extends StatelessWidget { ), ], builder: (BuildContext context, GoRouterState state, - StackedNavigationShell navigationShell) { + StatefulNavigationShell navigationShell) { /// This builder implementation uses the default container for the /// branch Navigators (provided in through the `child` argument). This - /// is the simplest way to use StackedShellRoute, where the shell is + /// is the simplest way to use StatefulShellRoute, where the shell is /// built around the Navigator container (see ScaffoldWithNavBar). return ScaffoldWithNavBar(navigationShell: navigationShell); }, - /// If it's necessary to customize the Page for StackedShellRoute, + /// If it's necessary to customize the Page for StatefulShellRoute, /// provide a pageBuilder function instead of the builder, for example: // pageBuilder: (BuildContext context, GoRouterState state, - // StackedNavigationShell navigationShell) { + // StatefulNavigationShell navigationShell) { // return NoTransitionPage( // child: ScaffoldWithNavBar(navigationShell: navigationShell)); // }, @@ -218,7 +218,7 @@ class ScaffoldWithNavBar extends StatelessWidget { }) : super(key: key ?? const ValueKey('ScaffoldWithNavBar')); /// The navigation shell and container for the branch Navigators. - final StackedNavigationShell navigationShell; + final StatefulNavigationShell navigationShell; @override Widget build(BuildContext context) { @@ -231,10 +231,22 @@ class ScaffoldWithNavBar extends StatelessWidget { BottomNavigationBarItem(icon: Icon(Icons.tab), label: 'Section C'), ], currentIndex: navigationShell.currentIndex, - onTap: (int tappedIndex) => navigationShell.goBranch(tappedIndex), + onTap: (int index) => _onTap(context, index), ), ); } + + void _onTap(BuildContext context, int index) { + // Navigate to the current location of branch at the provided index. If + // tapping the bar item for the current branch, go to the initial location + // instead. + if (index == navigationShell.currentIndex) { + GoRouter.of(context) + .go(navigationShell.effectiveInitialBranchLocation(index)); + } else { + navigationShell.goBranch(index); + } + } } /// Widget for the root/initial pages in the bottom navigation bar. @@ -460,8 +472,8 @@ class TabbedRootScreen extends StatefulWidget { const TabbedRootScreen( {required this.navigationShell, required this.children, super.key}); - /// The current state of the parent StackedShellRoute. - final StackedNavigationShell navigationShell; + /// The current state of the parent StatefulShellRoute. + final StatefulNavigationShell navigationShell; /// The children (Navigators) to display in the [TabBarView]. final List children; diff --git a/packages/go_router/lib/go_router.dart b/packages/go_router/lib/go_router.dart index 7a02fd3f3abb..c638b21ed51b 100644 --- a/packages/go_router/lib/go_router.dart +++ b/packages/go_router/lib/go_router.dart @@ -12,11 +12,12 @@ export 'src/configuration.dart' GoRouterState, RouteBase, ShellRoute, - StackedNavigationContainerBuilder, - StackedNavigationShell, - StackedNavigationShellState, - StackedShellBranch, - StackedShellRoute; + ShellNavigationContainerBuilder, + StackedShellRoute, + StatefulNavigationShell, + StatefulNavigationShellState, + StatefulShellBranch, + StatefulShellRoute; export 'src/misc/extensions.dart'; export 'src/misc/inherited_router.dart'; export 'src/pages/custom_transition_page.dart'; @@ -36,5 +37,5 @@ export 'src/typedefs.dart' GoRouterWidgetBuilder, ShellRouteBuilder, ShellRoutePageBuilder, - StackedShellRouteBuilder, - StackedShellRoutePageBuilder; + StatefulShellRouteBuilder, + StatefulShellRoutePageBuilder; diff --git a/packages/go_router/lib/src/builder.dart b/packages/go_router/lib/src/builder.dart index d937d2ce18aa..2c08e15cad43 100644 --- a/packages/go_router/lib/src/builder.dart +++ b/packages/go_router/lib/src/builder.dart @@ -21,7 +21,7 @@ import 'typedefs.dart'; /// /// This is a specialized version of [Navigator.onPopPage], used when creating /// Navigators in [RouteBuilder]. -typedef RouteBuilderPopPageCallback = bool Function( +typedef PopPageWithRouteMatchCallback = bool Function( Route route, dynamic result, RouteMatch? match); /// Builds the top-level Navigator for GoRouter. @@ -34,7 +34,7 @@ class RouteBuilder { required this.errorBuilder, required this.restorationScopeId, required this.observers, - required this.onPopPage, + required this.onPopPageWithRouteMatch, }); /// Builder function for a go router with Navigator. @@ -59,7 +59,7 @@ class RouteBuilder { /// Function used as [Navigator.onPopPage] callback, that additionally /// provides the [RouteMatch] associated with the popped Page. - final RouteBuilderPopPageCallback onPopPage; + final PopPageWithRouteMatchCallback onPopPageWithRouteMatch; final GoRouterStateRegistry _registry = GoRouterStateRegistry(); @@ -92,8 +92,8 @@ class RouteBuilder { return GoRouterStateRegistryScope( registry: _registry, child: result); } on _RouteBuilderError catch (e) { - return _buildErrorNavigator( - context, e, matchList, onPopPage, configuration.navigatorKey); + return _buildErrorNavigator(context, e, matchList.uri, + onPopPageWithRouteMatch, configuration.navigatorKey); } }, ), @@ -112,7 +112,8 @@ class RouteBuilder { GlobalKey navigatorKey, Map, GoRouterState> registry, ) { - final _PagePopContext pagePopContext = _PagePopContext._(onPopPage); + final _PagePopContext pagePopContext = + _PagePopContext._(onPopPageWithRouteMatch); return builderWithNav( context, _buildNavigator( @@ -148,14 +149,15 @@ class RouteBuilder { return keyToPage[navigatorKey]!; } on _RouteBuilderError catch (e) { return >[ - _buildErrorPage(context, e, matchList), + _buildErrorPage(context, e, matchList.uri), ]; } finally { /// Clean up previous cache to prevent memory leak, making sure any nested /// stateful shell routes for the current match list are kept. - final Iterable matchListShellRoutes = matchList.matches + final Iterable matchListShellRoutes = matchList + .matches .map((RouteMatch e) => e.route) - .whereType(); + .whereType(); final Set activeKeys = keyToPage.keys.toSet() ..addAll(_nestedStatefulNavigatorKeys(matchListShellRoutes)); @@ -165,11 +167,11 @@ class RouteBuilder { } Set> _nestedStatefulNavigatorKeys( - Iterable routes) { + Iterable routes) { return RouteBase.routesRecursively(routes) - .whereType() - .expand((StackedShellRoute e) => - e.branches.map((StackedShellBranch b) => b.navigatorKey)) + .whereType() + .expand((StatefulShellRoute e) => + e.branches.map((StatefulShellBranch b) => b.navigatorKey)) .toSet(); } @@ -353,8 +355,14 @@ class RouteBuilder { } page ??= buildPage(context, state, Builder(builder: (BuildContext context) { - return _callRouteBuilder(context, state, match, - shellNavigatorBuilder: shellRouteContext); + final RouteBase route = match.route; + if (route is GoRoute) { + return _callGoRouteBuilder(context, state, route); + } else if (route is ShellRouteBase) { + return _callShellRouteBaseBuilder( + context, state, route, shellRouteContext); + } + throw _RouteBuilderException('Unsupported route type $route'); })); pagePopContext._setRouteMatchForPage(page, match); @@ -362,33 +370,30 @@ class RouteBuilder { return page; } - /// Calls the user-provided route builder from the [RouteMatch]'s [RouteBase]. - Widget _callRouteBuilder( - BuildContext context, GoRouterState state, RouteMatch match, - {ShellRouteContext? shellNavigatorBuilder}) { - final RouteBase route = match.route; - - if (route is GoRoute) { - final GoRouterWidgetBuilder? builder = route.builder; + /// Calls the user-provided route builder from the [GoRoute]. + Widget _callGoRouteBuilder( + BuildContext context, GoRouterState state, GoRoute route) { + final GoRouterWidgetBuilder? builder = route.builder; - if (builder == null) { - throw _RouteBuilderError('No routeBuilder provided to GoRoute: $route'); - } + if (builder == null) { + throw _RouteBuilderError('No routeBuilder provided to GoRoute: $route'); + } - return builder(context, state); - } else if (route is ShellRouteBase) { - assert(shellNavigatorBuilder != null, - 'ShellRouteContext must be provided for ${route.runtimeType}'); - final Widget? widget = - route.buildWidget(context, state, shellNavigatorBuilder!); - if (widget == null) { - throw _RouteBuilderError('No builder provided to ShellRoute: $route'); - } + return builder(context, state); + } - return widget; + /// Calls the user-provided route builder from the [ShellRouteBase]. + Widget _callShellRouteBaseBuilder(BuildContext context, GoRouterState state, + ShellRouteBase route, ShellRouteContext? shellRouteContext) { + assert(shellRouteContext != null, + 'ShellRouteContext must be provided for ${route.runtimeType}'); + final Widget? widget = + route.buildWidget(context, state, shellRouteContext!); + if (widget == null) { + throw _RouteBuilderError('No builder provided to ShellRoute: $route'); } - throw _RouteBuilderException('Unsupported route type $route'); + return widget; } _PageBuilderForAppType? _pageBuilderForAppType; @@ -466,13 +471,13 @@ class RouteBuilder { Widget _buildErrorNavigator( BuildContext context, _RouteBuilderError e, - RouteMatchList matchList, - RouteBuilderPopPageCallback onPopPage, + Uri uri, + PopPageWithRouteMatchCallback onPopPage, GlobalKey navigatorKey) { return _buildNavigator( (Route route, dynamic result) => onPopPage(route, result, null), >[ - _buildErrorPage(context, e, matchList), + _buildErrorPage(context, e, uri), ], navigatorKey, ); @@ -482,9 +487,8 @@ class RouteBuilder { Page _buildErrorPage( BuildContext context, _RouteBuilderError error, - RouteMatchList matchList, + Uri uri, ) { - final Uri uri = matchList.uri; final GoRouterState state = GoRouterState( configuration, location: uri.toString(), @@ -573,13 +577,13 @@ class _RouteBuilderException implements Exception { /// Context used to provide a route to page association when popping routes. class _PagePopContext { - _PagePopContext._(this.routeBuilderOnPopPage); + _PagePopContext._(this.onPopPageWithRouteMatch); final Map, RouteMatch> _routeMatchLookUp = , RouteMatch>{}; /// On pop page callback that includes the associated [RouteMatch]. - final RouteBuilderPopPageCallback routeBuilderOnPopPage; + final PopPageWithRouteMatchCallback onPopPageWithRouteMatch; /// Looks for the [RouteMatch] for a given [Page]. /// @@ -593,10 +597,10 @@ class _PagePopContext { /// Function used as [Navigator.onPopPage] callback when creating Navigators. /// - /// This function forwards to [routeBuilderOnPopPage], including the + /// This function forwards to [onPopPageWithRouteMatch], including the /// [RouteMatch] associated with the popped route. bool onPopPage(Route route, dynamic result) { final Page page = route.settings as Page; - return routeBuilderOnPopPage(route, result, _routeMatchLookUp[page]); + return onPopPageWithRouteMatch(route, result, _routeMatchLookUp[page]); } } diff --git a/packages/go_router/lib/src/configuration.dart b/packages/go_router/lib/src/configuration.dart index c49a851fd79d..9faa0473a7df 100644 --- a/packages/go_router/lib/src/configuration.dart +++ b/packages/go_router/lib/src/configuration.dart @@ -27,7 +27,7 @@ class RouteConfiguration { _debugVerifyNoDuplicatePathParameter(routes, {})), assert(_debugCheckParentNavigatorKeys( routes, >[navigatorKey])) { - assert(_debugCheckStackedShellBranchDefaultLocations( + assert(_debugCheckStatefulShellBranchDefaultLocations( routes, RouteMatcher(this))); _cacheNameToPath('', routes); log.info(debugKnownRoutes()); @@ -90,11 +90,11 @@ class RouteConfiguration { route.routes, >[...allowedKeys..add(route.navigatorKey)], ); - } else if (route is StackedShellRoute) { - for (final StackedShellBranch branch in route.branches) { + } else if (route is StatefulShellRoute) { + for (final StatefulShellBranch branch in route.branches) { assert( !allowedKeys.contains(branch.navigatorKey), - 'StackedShellBranch must not reuse an ancestor navigatorKey ' + 'StatefulShellBranch must not reuse an ancestor navigatorKey ' '(${branch.navigatorKey})'); _debugCheckParentNavigatorKeys( @@ -130,14 +130,14 @@ class RouteConfiguration { return true; } - // Check to see that the configured initialLocation of StackedShellBranches + // Check to see that the configured initialLocation of StatefulShellBranches // points to a descendant route of the route branch. - bool _debugCheckStackedShellBranchDefaultLocations( + bool _debugCheckStatefulShellBranchDefaultLocations( List routes, RouteMatcher matcher) { try { for (final RouteBase route in routes) { - if (route is StackedShellRoute) { - for (final StackedShellBranch branch in route.branches) { + 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. @@ -146,7 +146,7 @@ class RouteConfiguration { route != null ? locationForRoute(route) : null; assert( initialLocation != null, - 'The initial location of a StackedShellBranch must be ' + 'The initial location of a StatefulShellBranch must be ' 'derivable from GoRoute descendant'); } else { final RouteBase initialLocationRoute = @@ -157,92 +157,28 @@ class RouteConfiguration { assert( match != null, 'The initialLocation (${branch.initialLocation}) of ' - 'StackedShellBranch must match a descendant route of the ' + 'StatefulShellBranch must match a descendant route of the ' 'branch'); } } } - _debugCheckStackedShellBranchDefaultLocations(route.routes, matcher); + _debugCheckStatefulShellBranchDefaultLocations(route.routes, matcher); } } on MatcherError catch (e) { assert( false, - 'initialLocation (${e.location}) of StackedShellBranch must ' + 'initialLocation (${e.location}) of StatefulShellBranch must ' 'be a valid location'); } return true; } - // /// Returns an Iterable that traverses the provided routes and their - // /// sub-routes recursively. - // static Iterable routesRecursively( - // Iterable routes) sync* { - // for (final RouteBase route in routes) { - // yield route; - // yield* routesRecursively(route.routes); - // } - // } - // - // static Iterable routesRecursively2(Iterable routes) { - // return routes.expand((RouteBase e) => [e, ...routesRecursively2(e.routes)]); - // } - - // static GoRoute? _findFirstGoRoute(List routes) => - // routesRecursively(routes).whereType().firstOrNull; - /// Tests if a route is a descendant of, or same as, an ancestor route. bool _debugIsDescendantOrSame( {required RouteBase ancestor, required RouteBase route}) => ancestor == route || RouteBase.routesRecursively(ancestor.routes).contains(route); - // /// Recursively traverses the routes of the provided StackedShellBranch to - // /// find the first GoRoute, from which a full path will be derived. - // String findStackedShellBranchDefaultLocation(StackedShellBranch branch) { - // final GoRoute? route = _findFirstGoRoute(branch.routes); - // final String? initialLocation = - // route != null ? fullPathForRoute(route, '', routes) : null; - // assert( - // initialLocation != null, - // 'The initial location of a StackedShellBranch must be derivable from ' - // 'GoRoute descendant'); - // return initialLocation!; - // } - - // /// Returns the effective initial location of a StackedShellBranch. - // /// - // /// If the initial location of the branch is null, - // /// [findStackedShellBranchDefaultLocation] is used to calculate the initial - // /// location. - // String effectiveInitialBranchLocation(StackedShellBranch branch) { - // final String? initialLocation = branch.initialLocation; - // if (initialLocation != null) { - // return initialLocation; - // } else { - // return findStackedShellBranchDefaultLocation(branch); - // } - // } - - // static String? _fullPathForRoute( - // RouteBase targetRoute, String parentFullpath, List routes) { - // for (final RouteBase route in routes) { - // final String fullPath = (route is GoRoute) - // ? concatenatePaths(parentFullpath, route.path) - // : parentFullpath; - // - // if (route == targetRoute) { - // return fullPath; - // } else { - // final String? subRoutePath = - // _fullPathForRoute(targetRoute, fullPath, route.routes); - // if (subRoutePath != null) { - // return subRoutePath; - // } - // } - // } - // return null; - // } - /// The list of top level routes used by [GoRouterDelegate]. final List routes; diff --git a/packages/go_router/lib/src/delegate.dart b/packages/go_router/lib/src/delegate.dart index beb6daf9082d..f3eb1c0395e5 100644 --- a/packages/go_router/lib/src/delegate.dart +++ b/packages/go_router/lib/src/delegate.dart @@ -27,9 +27,7 @@ class GoRouterDelegate extends RouterDelegate required List observers, required this.routerNeglect, String? restorationScopeId, - }) : _configuration = configuration, - _restorablePropertiesRestorationId = - '${restorationScopeId ?? ''}._GoRouterDelegateRestorableProperties' { + }) : _configuration = configuration { builder = RouteBuilder( configuration: configuration, builderWithNav: builderWithNav, @@ -37,7 +35,7 @@ class GoRouterDelegate extends RouterDelegate errorBuilder: errorBuilder, restorationScopeId: restorationScopeId, observers: observers, - onPopPage: _onPopPage, + onPopPageWithRouteMatch: _handlePopPageWithRouteMatch, ); } @@ -52,12 +50,7 @@ class GoRouterDelegate extends RouterDelegate final RouteConfiguration _configuration; - final String _restorablePropertiesRestorationId; - final GlobalKey<_GoRouterDelegateRestorablePropertiesState> - _restorablePropertiesKey = - GlobalKey<_GoRouterDelegateRestorablePropertiesState>(); - - /// Increments the stored number of times each route route has been pushed. + /// Stores the number of times each route route has been pushed. /// /// This is used to generate a unique key for each route. /// @@ -68,18 +61,7 @@ class GoRouterDelegate extends RouterDelegate /// 'family/:fid': 2, /// } /// ``` - int _incrementPushCount(String path) { - final _GoRouterDelegateRestorablePropertiesState? - restorablePropertiesState = _restorablePropertiesKey.currentState; - assert(restorablePropertiesState != null); - - final Map pushCounts = - restorablePropertiesState!.pushCount.value; - final int count = (pushCounts[path] ?? -1) + 1; - pushCounts[path] = count; - restorablePropertiesState.pushCount.value = pushCounts; - return count; - } + final Map _pushCounts = {}; _NavigatorStateIterator _createNavigatorStateIterator() => _NavigatorStateIterator(_matchList, navigatorKey.currentState!); @@ -98,7 +80,8 @@ class GoRouterDelegate extends RouterDelegate ValueKey _getNewKeyForPath(String path) { // Remap the pageKey to allow any number of the same page on the stack - final int count = _incrementPushCount(path); + final int count = (_pushCounts[path] ?? -1) + 1; + _pushCounts[path] = count; return ValueKey('$path-p$count'); } @@ -161,7 +144,8 @@ class GoRouterDelegate extends RouterDelegate ); } - bool _onPopPage(Route route, Object? result, RouteMatch? match) { + bool _handlePopPageWithRouteMatch( + Route route, Object? result, RouteMatch? match) { if (!route.didPop(result)) { return false; } @@ -226,14 +210,10 @@ class GoRouterDelegate extends RouterDelegate /// For use by the Router architecture as part of the RouterDelegate. @override Widget build(BuildContext context) { - return _GoRouterDelegateRestorableProperties( - key: _restorablePropertiesKey, - restorationId: _restorablePropertiesRestorationId, - builder: (BuildContext context) => builder.build( - context, - _matchList, - routerNeglect, - ), + return builder.build( + context, + _matchList, + routerNeglect, ); } @@ -361,55 +341,3 @@ class ImperativeRouteMatch extends RouteMatch { /// When the future completes, this will return the value passed to [complete]. Future get _future => _completer.future; } - -class _GoRouterDelegateRestorableProperties extends StatefulWidget { - const _GoRouterDelegateRestorableProperties( - {required super.key, required this.restorationId, required this.builder}); - - final String restorationId; - final WidgetBuilder builder; - - @override - State createState() => - _GoRouterDelegateRestorablePropertiesState(); -} - -class _GoRouterDelegateRestorablePropertiesState - extends State<_GoRouterDelegateRestorableProperties> with RestorationMixin { - final _RestorablePushCount pushCount = _RestorablePushCount(); - - @override - String? get restorationId => widget.restorationId; - - @override - void restoreState(RestorationBucket? oldBucket, bool initialRestore) { - registerForRestoration(pushCount, 'push_count'); - } - - @override - Widget build(BuildContext context) { - return widget.builder(context); - } -} - -class _RestorablePushCount extends RestorableValue> { - @override - Map createDefaultValue() => {}; - - @override - Map fromPrimitives(Object? data) { - if (data is Map) { - return data.map((Object? key, Object? value) => - MapEntry(key! as String, value! as int)); - } - return {}; - } - - @override - Object? toPrimitives() => value; - - @override - void didUpdateValue(Map? oldValue) { - notifyListeners(); - } -} diff --git a/packages/go_router/lib/src/route.dart b/packages/go_router/lib/src/route.dart index 26c0b584cd72..796f02d7a85e 100644 --- a/packages/go_router/lib/src/route.dart +++ b/packages/go_router/lib/src/route.dart @@ -329,7 +329,7 @@ class GoRoute extends RouteBase { } /// Base class for classes that act as shells for sub-routes, such -/// as [ShellRoute] and [StackedShellRoute]. +/// as [ShellRoute] and [StatefulShellRoute]. abstract class ShellRouteBase extends RouteBase { /// Constructs a [ShellRouteBase]. const ShellRouteBase._({super.routes}) : super._(); @@ -571,19 +571,19 @@ class ShellRoute extends ShellRouteBase { /// implementing a UI with a [BottomNavigationBar], with a persistent navigation /// state for each tab. /// -/// A StackedShellRoute is created by specifying a List of -/// [StackedShellBranch] items, each representing a separate stateful branch -/// in the route tree. StackedShellBranch provides the root routes and the +/// A StatefulShellRoute is created by specifying a List of +/// [StatefulShellBranch] items, each representing a separate stateful branch +/// in the route tree. StatefulShellBranch provides the root routes and the /// Navigator key ([GlobalKey]) for the branch, as well as an optional initial /// location. /// /// Like [ShellRoute], either a [builder] or a [pageBuilder] must be provided -/// when creating a StackedShellRoute. However, these builders differ slightly -/// in that they accept a [StackedNavigationShell] parameter instead of a -/// child Widget. The StackedNavigationShell can be used to access information +/// when creating a StatefulShellRoute. However, these builders differ slightly +/// 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 [StackedNavigationShell.goBranch], for +/// accomplished by using the method [StatefulNavigationShell.goBranch], for /// example: /// /// ``` @@ -592,18 +592,23 @@ class ShellRoute extends ShellRouteBase { /// } /// ``` /// -/// The StackedNavigationShell is also responsible for managing and maintaining +/// The StatefulNavigationShell is also responsible for managing and maintaining /// the state of the branch Navigators. Typically, a shell is built around this /// Widget, for example by using it as the body of [Scaffold] with a /// [BottomNavigationBar]. /// -/// Sometimes greater control is needed over the layout and animations of the -/// Widgets representing the branch Navigators. In such cases, a custom -/// implementation can choose to provide a [navigatorContainerBuilder], in -/// which a custom container Widget can be provided for the branch Navigators. +/// When creating a StatefulShellRoute, a [navigatorContainerBuilder] function +/// must be provided. This function is responsible for building the actual +/// container for the Widgets representing the branch Navigators. Typically, +/// the Widget returned by this function handles the layout (including +/// [Offstage] handling etc) of the branch Navigators and any animations needed +/// when switching active branch. +/// +/// For a default implementation of [navigatorContainerBuilder], consider using +/// [StackedShellRoute]. /// /// Below is a simple example of how a router configuration with -/// StackedShellRoute could be achieved. In this example, a +/// StatefulShellRoute could be achieved. In this example, a /// BottomNavigationBar with two tabs is used, and each of the tabs gets its /// own Navigator. A container widget responsible for managing the Navigators /// for all route branches will then be passed as the child argument @@ -618,14 +623,18 @@ class ShellRoute extends ShellRouteBase { /// final GoRouter _router = GoRouter( /// initialLocation: '/a', /// routes: [ -/// StackedShellRoute( +/// StatefulShellRoute( /// builder: (BuildContext context, GoRouterState state, -/// StackedNavigationShell navigationShell) { +/// StatefulNavigationShell navigationShell) { /// return ScaffoldWithNavBar(navigationShell: navigationShell); /// }, +/// navigatorContainerBuilder: (BuildContext context, +/// StatefulNavigationShell navigationShell, +/// List children) => +/// MyCustomContainer(children: children), /// branches: [ /// /// The first branch, i.e. tab 'A' -/// StackedShellBranch( +/// StatefulShellBranch( /// navigatorKey: _tabANavigatorKey, /// routes: [ /// GoRoute( @@ -644,7 +653,7 @@ class ShellRoute extends ShellRouteBase { /// ], /// ), /// /// The second branch, i.e. tab 'B' -/// StackedShellBranch( +/// StatefulShellBranch( /// navigatorKey: _tabBNavigatorKey, /// routes: [ /// GoRoute( @@ -669,21 +678,20 @@ class ShellRoute 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 StackedShellRoute. -class StackedShellRoute extends ShellRouteBase { - /// Constructs a [StackedShellRoute] from a list of [StackedShellBranch]es, +/// for a complete runnable example using StatefulShellRoute and StackedShellRoute. +class StatefulShellRoute extends ShellRouteBase { + /// Constructs a [StatefulShellRoute] from a list of [StatefulShellBranch]es, /// each representing a separate nested navigation tree (branch). /// /// A separate [Navigator] will be created for each of the branches, using - /// the navigator key specified in [StackedShellBranch]. Note that unlike - /// [ShellRoute], a builder must always be provided when creating a - /// StackedShellRoute. The pageBuilder however is optional, and is used - /// in addition to the builder. - StackedShellRoute({ + /// the navigator key specified in [StatefulShellBranch]. The Widget + /// implementing the container for the branch Navigators is provided by + /// [navigatorContainerBuilder]. + StatefulShellRoute({ required this.branches, this.builder, this.pageBuilder, - this.navigatorContainerBuilder, + required this.navigatorContainerBuilder, this.restorationScopeId, }) : assert(branches.isNotEmpty), assert((pageBuilder != null) ^ (builder != null), @@ -701,55 +709,54 @@ class StackedShellRoute extends ShellRouteBase { /// The widget builder for a stateful shell route. /// /// Similar to [GoRoute.builder], but with an additional - /// [StackedNavigationShell] parameter. StackedNavigationShell is a Widget + /// [StatefulNavigationShell] parameter. StatefulNavigationShell is a Widget /// responsible for managing the nested navigation for the /// matching sub-routes. Typically, a shell route builds its shell around this - /// Widget. StackedNavigationShell can also be used to access information + /// Widget. StatefulNavigationShell can also be used to access information /// about which branch is active, and also to navigate to a different branch - /// (using [StackedNavigationShell.goBranch]). + /// (using [StatefulNavigationShell.goBranch]). /// /// Custom implementations may choose to ignore the child parameter passed to - /// the builder function, and instead use [StackedNavigationShell] to + /// the builder function, and instead use [StatefulNavigationShell] to /// create a custom container for the branch Navigators. - final StackedShellRouteBuilder? builder; + final StatefulShellRouteBuilder? builder; /// The page builder for a stateful shell route. /// /// Similar to [GoRoute.pageBuilder], but with an additional - /// [StackedNavigationShell] parameter. StackedNavigationShell is a Widget + /// [StatefulNavigationShell] parameter. StatefulNavigationShell is a Widget /// responsible for managing the nested navigation for the /// matching sub-routes. Typically, a shell route builds its shell around this - /// Widget. StackedNavigationShell can also be used to access information + /// Widget. StatefulNavigationShell can also be used to access information /// about which branch is active, and also to navigate to a different branch - /// (using [StackedNavigationShell.goBranch]). + /// (using [StatefulNavigationShell.goBranch]). /// /// Custom implementations may choose to ignore the child parameter passed to - /// the builder function, and instead use [StackedNavigationShell] to + /// the builder function, and instead use [StatefulNavigationShell] to /// create a custom container for the branch Navigators. - final StackedShellRoutePageBuilder? pageBuilder; + final StatefulShellRoutePageBuilder? pageBuilder; - /// An optional builder for a custom container for the branch Navigators. + /// The builder for the branch Navigator container. /// - /// StackedShellRoute provides a default implementation for managing the - /// Widgets representing the branch Navigators, but in some cases a different - /// implementation may be required. When providing an implementation for this - /// builder, access is provided to a List of Widgets representing the branch - /// Navigators, where the the index corresponds to the index of in [branches]. + /// The function responsible for building the container for the branch + /// Navigators. When this function is invoked, access is provided to a List of + /// Widgets representing the branch Navigators, where the the index + /// corresponds to the index of in [branches]. /// /// The builder function is expected to return a Widget that ensures that the /// state of the branch Widgets is maintained, for instance by inducting them /// in the Widget tree. - final StackedNavigationContainerBuilder? navigatorContainerBuilder; + final ShellNavigationContainerBuilder navigatorContainerBuilder; /// Representations of the different stateful route branches that this /// shell route will manage. /// /// Each branch uses a separate [Navigator], identified - /// [StackedShellBranch.navigatorKey]. - final List branches; + /// [StatefulShellBranch.navigatorKey]. + final List branches; - final GlobalKey _shellStateKey = - GlobalKey(); + final GlobalKey _shellStateKey = + GlobalKey(); @override Widget? buildWidget(BuildContext context, GoRouterState state, @@ -772,30 +779,30 @@ class StackedShellRoute extends ShellRouteBase { @override GlobalKey navigatorKeyForSubRoute(RouteBase subRoute) { - final StackedShellBranch? branch = branches.firstWhereOrNull( - (StackedShellBranch e) => e.routes.contains(subRoute)); + final StatefulShellBranch? branch = branches.firstWhereOrNull( + (StatefulShellBranch e) => e.routes.contains(subRoute)); assert(branch != null); return branch!.navigatorKey; } - StackedNavigationShell _createShell( + StatefulNavigationShell _createShell( BuildContext context, ShellRouteContext shellRouteContext) => - StackedNavigationShell( + StatefulNavigationShell( shellRouteContext: shellRouteContext, router: GoRouter.of(context), containerBuilder: navigatorContainerBuilder); - static List _routes(List branches) => - branches.expand((StackedShellBranch e) => e.routes).toList(); + static List _routes(List branches) => + branches.expand((StatefulShellBranch e) => e.routes).toList(); static Set> _debugUniqueNavigatorKeys( - List branches) => + List branches) => Set>.from( - branches.map((StackedShellBranch e) => e.navigatorKey)); + branches.map((StatefulShellBranch e) => e.navigatorKey)); static bool _debugValidateParentNavigatorKeys( - List branches) { - for (final StackedShellBranch branch in branches) { + List branches) { + for (final StatefulShellBranch branch in branches) { for (final RouteBase route in branch.routes) { if (route is GoRoute) { assert(route.parentNavigatorKey == null || @@ -807,40 +814,68 @@ class StackedShellRoute extends ShellRouteBase { } static bool _debugValidateRestorationScopeIds( - String? restorationScopeId, List branches) { + String? restorationScopeId, List branches) { if (branches - .map((StackedShellBranch e) => e.restorationScopeId) + .map((StatefulShellBranch e) => e.restorationScopeId) .whereNotNull() .isNotEmpty) { assert( restorationScopeId != null, 'A restorationScopeId must be set for ' - 'the StackedShellRoute when using restorationScopeIds on one or more ' + 'the StatefulShellRoute when using restorationScopeIds on one or more ' 'of the branches'); } return true; } } +/// A stateful shell route implementation that uses an [IndexedStack] for its +/// nested [Navigator]s. +/// +/// StackedShellRoute provides an IndexedStack based implementation for the +/// container ([navigatorContainerBuilder]) used to managing the Widgets +/// representing the branch Navigators. StackedShellRoute is created in the same +/// way as [StatefulShellRoute], but without the need to provide a +/// navigatorContainerBuilder parameter. +/// +/// 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 and StackedShellRoute. +class StackedShellRoute extends StatefulShellRoute { + /// Constructs a [StackedShellRoute] from a list of [StatefulShellBranch]es, + /// each representing a separate nested navigation tree (branch). + StackedShellRoute({ + required super.branches, + super.builder, + super.pageBuilder, + super.restorationScopeId, + }) : super(navigatorContainerBuilder: _navigatorContainerBuilder); + + static Widget _navigatorContainerBuilder(BuildContext context, + StatefulNavigationShell navigationShell, List children) { + return _IndexedStackedRouteBranchContainer( + currentIndex: navigationShell.currentIndex, children: children); + } +} + /// Representation of a separate branch in a stateful navigation tree, used to -/// configure [StackedShellRoute]. +/// configure [StatefulShellRoute]. /// -/// The only required argument when creating a StackedShellBranch is the +/// The only required argument when creating a StatefulShellBranch is the /// 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 [StackedNavigationShell]). +/// using the goBranch method in [StatefulNavigationShell]). /// -/// A separate [Navigator] will be built for each StackedShellBranch in a -/// [StackedShellRoute], and the routes of this branch will be placed onto that +/// A separate [Navigator] will be built for each StatefulShellBranch in a +/// [StatefulShellRoute], and the routes of this branch will be placed onto that /// Navigator instead of the root Navigator. A custom [navigatorKey] can be -/// provided when creating a StackedShellBranch, which can be useful when the +/// provided when creating a StatefulShellBranch, which can be useful when the /// Navigator needs to be accessed elsewhere. If no key is provided, a default /// one will be created. @immutable -class StackedShellBranch { - /// Constructs a [StackedShellBranch]. - StackedShellBranch({ +class StatefulShellBranch { + /// Constructs a [StatefulShellBranch]. + StatefulShellBranch({ required this.routes, GlobalKey? navigatorKey, this.initialLocation, @@ -850,8 +885,8 @@ class StackedShellBranch { /// The [GlobalKey] to be used by the [Navigator] built for this branch. /// - /// A separate Navigator will be built for each StackedShellBranch in a - /// [StackedShellRoute] and this key will be used to identify the Navigator. + /// A separate Navigator will be built for each StatefulShellBranch in a + /// [StatefulShellRoute] and this key will be used to identify the Navigator. /// The routes associated with this branch will be placed o onto that /// Navigator instead of the root Navigator. final GlobalKey navigatorKey; @@ -885,86 +920,105 @@ class StackedShellBranch { } /// Builder for a custom container for the branch Navigators of a -/// [StackedShellRoute]. -typedef StackedNavigationContainerBuilder = Widget Function( - BuildContext context, - StackedNavigationShell navigationShell, - List children); +/// [StatefulShellRoute]. +typedef ShellNavigationContainerBuilder = Widget Function(BuildContext context, + StatefulNavigationShell navigationShell, List children); -/// Widget for managing the state of a [StackedShellRoute]. +/// Widget for managing the state of a [StatefulShellRoute]. /// /// Normally, this widget is not used directly, but is instead created -/// internally by StackedShellRoute. However, if a custom container for the -/// branch Navigators is required, StackedNavigationShell can be used in -/// the builder or pageBuilder methods of StackedShellRoute to facilitate this. -/// The container is created using the provided [StackedNavigationContainerBuilder], +/// 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, -/// StackedNavigationShell navigationShell) { -/// return StackedNavigationShell( +/// StatefulNavigationShell navigationShell) { +/// return StatefulNavigationShell( /// shellRouteState: state, /// containerBuilder: (_, __, List children) => MyCustomShell(shellState: state, children: children), /// ); /// } /// ``` -class StackedNavigationShell extends StatefulWidget { - /// Constructs an [_StackedNavigationShell]. - StackedNavigationShell({ +class StatefulNavigationShell extends StatefulWidget { + /// Constructs an [StatefulNavigationShell]. + StatefulNavigationShell({ required this.shellRouteContext, required GoRouter router, - this.containerBuilder, - }) : assert(shellRouteContext.route is StackedShellRoute), + required this.containerBuilder, + }) : assert(shellRouteContext.route is StatefulShellRoute), _router = router, currentIndex = _indexOfBranchNavigatorKey( - shellRouteContext.route as StackedShellRoute, + shellRouteContext.route as StatefulShellRoute, shellRouteContext.navigatorKey), super( - key: (shellRouteContext.route as StackedShellRoute)._shellStateKey); + key: + (shellRouteContext.route as StatefulShellRoute)._shellStateKey); /// The ShellRouteContext responsible for building the Navigator for the - /// current [StackedShellBranch] + /// current [StatefulShellBranch] final ShellRouteContext shellRouteContext; /// The builder for a custom container for shell route Navigators. - final StackedNavigationContainerBuilder? containerBuilder; + final ShellNavigationContainerBuilder containerBuilder; - /// The index of the currently active [StackedShellBranch]. + /// The index of the currently active [StatefulShellBranch]. /// - /// Corresponds to the index in the branches field of [StackedShellRoute]. + /// Corresponds to the index in the branches field of [StatefulShellRoute]. final int currentIndex; final GoRouter _router; - /// Navigate to the last location of the [StackedShellBranch] at the provided - /// index in the associated [StackedShellBranch]. + /// 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 - /// [StackedShellRoute]. If the branch has not been visited before, this + /// [StatefulShellRoute]. If the branch has not been visited before, this /// method will navigate to initial location of the branch (see - /// [StackedShellBranch.initialLocation]). + /// [StatefulShellBranch.initialLocation]). void goBranch(int index) { - final StackedShellRoute route = - shellRouteContext.route as StackedShellRoute; - final StackedNavigationShellState? shellState = + final StatefulShellRoute route = + shellRouteContext.route as StatefulShellRoute; + final StatefulNavigationShellState? shellState = route._shellStateKey.currentState; if (shellState != null) { shellState.goBranch(index); } else { - _router.go(StackedNavigationShellState._effectiveInitialBranchLocation( - _router, route, index)); + _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!; + return _router.locationForRoute(route)!; } } @override - State createState() => StackedNavigationShellState(); + State createState() => StatefulNavigationShellState(); /// Gets the state for the nearest stateful shell route in the Widget tree. - static StackedNavigationShellState of(BuildContext context) { - final StackedNavigationShellState? shellState = - context.findAncestorStateOfType(); + static StatefulNavigationShellState of(BuildContext context) { + final StatefulNavigationShellState? shellState = + context.findAncestorStateOfType(); assert(shellState != null); return shellState!; } @@ -972,34 +1026,34 @@ class StackedNavigationShell extends StatefulWidget { /// Gets the state for the nearest stateful shell route in the Widget tree. /// /// Returns null if no stateful shell route is found. - static StackedNavigationShellState? maybeOf(BuildContext context) { - final StackedNavigationShellState? shellState = - context.findAncestorStateOfType(); + static StatefulNavigationShellState? maybeOf(BuildContext context) { + final StatefulNavigationShellState? shellState = + context.findAncestorStateOfType(); return shellState; } static int _indexOfBranchNavigatorKey( - StackedShellRoute route, GlobalKey navigatorKey) { + StatefulShellRoute route, GlobalKey navigatorKey) { final int index = route.branches.indexWhere( - (StackedShellBranch branch) => branch.navigatorKey == navigatorKey); + (StatefulShellBranch branch) => branch.navigatorKey == navigatorKey); assert(index >= 0); return index; } } -/// State for StackedNavigationShell. -class StackedNavigationShellState extends State +/// State for StatefulNavigationShell. +class StatefulNavigationShellState extends State with RestorationMixin { final Map _branchNavigators = {}; - StackedShellRoute get _route => - widget.shellRouteContext.route as StackedShellRoute; + StatefulShellRoute get _route => + widget.shellRouteContext.route as StatefulShellRoute; GoRouter get _router => widget._router; RouteMatcher get _matcher => _router.routeInformationParser.matcher; - final Map _branchLocations = - {}; + final Map _branchLocations = + {}; @override String? get restorationId => _route.restorationScopeId; @@ -1007,13 +1061,13 @@ class StackedNavigationShellState extends State /// 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 /// always returned (needed for _RestorableRouteMatchList/RestorableValue). - String _branchLocationRestorationScopeId(StackedShellBranch branch) { + String _branchLocationRestorationScopeId(StatefulShellBranch branch) { return branch.restorationScopeId != null ? '${branch.restorationScopeId}-location' : identityHashCode(branch).toString(); } - _RestorableRouteMatchList _branchLocation(StackedShellBranch branch, + _RestorableRouteMatchList _branchLocation(StatefulShellBranch branch, [bool register = true]) { return _branchLocations.putIfAbsent(branch, () { final _RestorableRouteMatchList branchLocation = @@ -1030,7 +1084,7 @@ class StackedNavigationShellState extends State _branchLocations[_route.branches[index]]?.value; void _updateCurrentBranchStateFromWidget() { - final StackedShellBranch branch = _route.branches[widget.currentIndex]; + final StatefulShellBranch branch = _route.branches[widget.currentIndex]; final ShellRouteContext shellRouteContext = widget.shellRouteContext; /// Create an clone of the current RouteMatchList, to prevent mutations from @@ -1054,22 +1108,16 @@ class StackedNavigationShellState extends State } } - Widget _indexedStackChildBuilder(BuildContext context, - StackedNavigationShell navigationShell, List children) { - return _IndexedStackedRouteBranchContainer( - currentIndex: widget.currentIndex, children: children); - } - - /// The index of the currently active [StackedShellBranch]. + /// The index of the currently active [StatefulShellBranch]. /// - /// Corresponds to the index in the branches field of [StackedShellRoute]. + /// Corresponds to the index in the branches field of [StatefulShellRoute]. int get currentIndex => widget.currentIndex; - /// Navigate to the last location of the [StackedShellBranch] at the provided - /// index in the associated [StackedShellBranch]. + /// 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 - /// [StackedShellRoute]. If the branch has not been visited before, this + /// [StatefulShellRoute]. If the branch has not been visited before, this /// method will navigate to initial location of the branch (see void goBranch(int index) { assert(index >= 0 && index < _route.branches.length); @@ -1079,21 +1127,7 @@ class StackedNavigationShellState extends State matchlist.toPreParsedRouteInformation(); _router.go(preParsed.location!, extra: preParsed.state); } else { - _router.go(_effectiveInitialBranchLocation(_router, _route, index)); - } - } - - static String _effectiveInitialBranchLocation( - GoRouter router, StackedShellRoute route, int index) { - final StackedShellBranch 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.locationForRoute(route)!; + _router.go(widget.effectiveInitialBranchLocation(index)); } } @@ -1106,7 +1140,7 @@ class StackedNavigationShellState extends State @override void dispose() { super.dispose(); - for (final StackedShellBranch branch in _route.branches) { + for (final StatefulShellBranch branch in _route.branches) { _branchLocations[branch]?.dispose(); } } @@ -1117,7 +1151,7 @@ class StackedNavigationShellState extends State } @override - void didUpdateWidget(covariant StackedNavigationShell oldWidget) { + void didUpdateWidget(covariant StatefulNavigationShell oldWidget) { super.didUpdateWidget(oldWidget); _updateCurrentBranchStateFromWidget(); } @@ -1125,16 +1159,14 @@ class StackedNavigationShellState extends State @override Widget build(BuildContext context) { final List children = _route.branches - .map((StackedShellBranch branch) => _BranchNavigatorProxy( + .map((StatefulShellBranch branch) => _BranchNavigatorProxy( key: ObjectKey(branch), branch: branch, - navigatorForBranch: (StackedShellBranch b) => + navigatorForBranch: (StatefulShellBranch b) => _branchNavigators[b.navigatorKey])) .toList(); - final StackedNavigationContainerBuilder containerBuilder = - widget.containerBuilder ?? _indexedStackChildBuilder; - return containerBuilder(context, widget, children); + return widget.containerBuilder(context, widget, children); } } @@ -1176,7 +1208,7 @@ class _RestorableRouteMatchList extends RestorableProperty { } } -typedef _NavigatorForBranch = Widget? Function(StackedShellBranch); +typedef _NavigatorForBranch = Widget? Function(StatefulShellBranch); /// Widget that serves as the proxy for a branch Navigator Widget, which /// possibly hasn't been created yet. @@ -1193,7 +1225,7 @@ class _BranchNavigatorProxy extends StatefulWidget { required this.navigatorForBranch, }); - final StackedShellBranch branch; + final StatefulShellBranch branch; final _NavigatorForBranch navigatorForBranch; @override diff --git a/packages/go_router/lib/src/typedefs.dart b/packages/go_router/lib/src/typedefs.dart index 9b214957d23b..7a4071529f03 100644 --- a/packages/go_router/lib/src/typedefs.dart +++ b/packages/go_router/lib/src/typedefs.dart @@ -34,18 +34,18 @@ typedef ShellRoutePageBuilder = Page Function( Widget child, ); -/// The widget builder for [StackedShellRoute]. -typedef StackedShellRouteBuilder = Widget Function( +/// The widget builder for [StatefulShellRoute]. +typedef StatefulShellRouteBuilder = Widget Function( BuildContext context, GoRouterState state, - StackedNavigationShell navigationShell, + StatefulNavigationShell navigationShell, ); -/// The page builder for [StackedShellRoute]. -typedef StackedShellRoutePageBuilder = Page Function( +/// The page builder for [StatefulShellRoute]. +typedef StatefulShellRoutePageBuilder = Page Function( BuildContext context, GoRouterState state, - StackedNavigationShell navigationShell, + StatefulNavigationShell navigationShell, ); /// Signature of a go router builder function with navigator. diff --git a/packages/go_router/test/builder_test.dart b/packages/go_router/test/builder_test.dart index 7ed7d0c88558..693670c8ff30 100644 --- a/packages/go_router/test/builder_test.dart +++ b/packages/go_router/test/builder_test.dart @@ -325,7 +325,7 @@ void main() { 'scope1'); }); - testWidgets('Uses the correct restorationScopeId for StackedShellRoute', + testWidgets('Uses the correct restorationScopeId for StatefulShellRoute', (WidgetTester tester) async { final GlobalKey rootNavigatorKey = GlobalKey(debugLabel: 'root'); @@ -338,10 +338,10 @@ void main() { StackedShellRoute( restorationScopeId: 'shell', builder: (BuildContext context, GoRouterState state, - StackedNavigationShell navigationShell) => + StatefulNavigationShell navigationShell) => _HomeScreen(child: navigationShell), - branches: [ - StackedShellBranch( + branches: [ + StatefulShellBranch( navigatorKey: shellNavigatorKey, restorationScopeId: 'scope1', routes: [ @@ -436,7 +436,7 @@ class _BuilderTestWidget extends StatelessWidget { }, restorationScopeId: null, observers: [], - onPopPage: (_, __, ___) => false, + onPopPageWithRouteMatch: (_, __, ___) => false, ); } diff --git a/packages/go_router/test/configuration_test.dart b/packages/go_router/test/configuration_test.dart index 1941bcb59010..20a4ba36544b 100644 --- a/packages/go_router/test/configuration_test.dart +++ b/packages/go_router/test/configuration_test.dart @@ -97,8 +97,8 @@ void main() { RouteConfiguration( navigatorKey: root, routes: [ - StackedShellRoute(branches: [ - StackedShellBranch( + StackedShellRoute(branches: [ + StatefulShellBranch( navigatorKey: keyA, routes: [ GoRoute( @@ -135,8 +135,8 @@ void main() { RouteConfiguration( navigatorKey: root, routes: [ - StackedShellRoute(branches: [ - StackedShellBranch( + StackedShellRoute(branches: [ + StatefulShellBranch( navigatorKey: keyA, routes: [ GoRoute( @@ -171,8 +171,8 @@ void main() { RouteConfiguration( navigatorKey: root, routes: [ - StackedShellRoute(branches: [ - StackedShellBranch( + StackedShellRoute(branches: [ + StatefulShellBranch( routes: [ GoRoute( path: '/a', @@ -185,7 +185,7 @@ void main() { ]), ], ), - StackedShellBranch( + StatefulShellBranch( routes: [ GoRoute( path: '/b', @@ -221,8 +221,8 @@ void main() { RouteConfiguration( navigatorKey: root, routes: [ - StackedShellRoute(branches: [ - StackedShellBranch(routes: shellRouteChildren) + StackedShellRoute(branches: [ + StatefulShellBranch(routes: shellRouteChildren) ], builder: mockStackedShellBuilder), ], redirectLimit: 10, @@ -257,11 +257,11 @@ void main() { RouteConfiguration( navigatorKey: root, routes: [ - StackedShellRoute(branches: [ - StackedShellBranch( + StackedShellRoute(branches: [ + StatefulShellBranch( routes: [routeA], navigatorKey: sectionANavigatorKey), - StackedShellBranch( + StatefulShellBranch( routes: [routeB], navigatorKey: sectionBNavigatorKey), ], builder: mockStackedShellBuilder), @@ -290,8 +290,8 @@ void main() { RouteConfiguration( navigatorKey: root, routes: [ - StackedShellRoute(branches: [ - StackedShellBranch( + StackedShellRoute(branches: [ + StatefulShellBranch( initialLocation: '/x', navigatorKey: sectionANavigatorKey, routes: [ @@ -301,7 +301,7 @@ void main() { ), ], ), - StackedShellBranch( + StatefulShellBranch( navigatorKey: sectionBNavigatorKey, routes: [ GoRoute( @@ -336,8 +336,8 @@ void main() { RouteConfiguration( navigatorKey: root, routes: [ - StackedShellRoute(branches: [ - StackedShellBranch( + StackedShellRoute(branches: [ + StatefulShellBranch( initialLocation: '/b', navigatorKey: sectionANavigatorKey, routes: [ @@ -347,12 +347,12 @@ void main() { ), ], ), - StackedShellBranch( + StatefulShellBranch( initialLocation: '/b', navigatorKey: sectionBNavigatorKey, routes: [ - StackedShellRoute(branches: [ - StackedShellBranch( + StackedShellRoute(branches: [ + StatefulShellBranch( routes: [ GoRoute( path: '/b', @@ -384,8 +384,8 @@ void main() { RouteConfiguration( navigatorKey: root, routes: [ - StackedShellRoute(branches: [ - StackedShellBranch( + StackedShellRoute(branches: [ + StatefulShellBranch( routes: [ GoRoute( path: '/a', @@ -398,7 +398,7 @@ void main() { ]), ], ), - StackedShellBranch( + StatefulShellBranch( initialLocation: '/b/detail', routes: [ GoRoute( @@ -412,11 +412,11 @@ void main() { ]), ], ), - StackedShellBranch( + StatefulShellBranch( initialLocation: '/c/detail', routes: [ - StackedShellRoute(branches: [ - StackedShellBranch( + StackedShellRoute(branches: [ + StatefulShellBranch( routes: [ GoRoute( path: '/c', @@ -429,7 +429,7 @@ void main() { ]), ], ), - StackedShellBranch( + StatefulShellBranch( initialLocation: '/d/detail', routes: [ GoRoute( @@ -446,7 +446,7 @@ void main() { ], builder: mockStackedShellBuilder), ], ), - StackedShellBranch(routes: [ + StatefulShellBranch(routes: [ ShellRoute( builder: _mockShellBuilder, routes: [ @@ -472,19 +472,19 @@ void main() { }); test( - 'derives the correct initialLocation for a StackedShellBranch', + 'derives the correct initialLocation for a StatefulShellBranch', () { - final StackedShellBranch branchA; - final StackedShellBranch branchY; - final StackedShellBranch branchB; + final StatefulShellBranch branchA; + final StatefulShellBranch branchY; + final StatefulShellBranch branchB; final RouteConfiguration config = RouteConfiguration( navigatorKey: GlobalKey(debugLabel: 'root'), routes: [ StackedShellRoute( builder: mockStackedShellBuilder, - branches: [ - branchA = StackedShellBranch(routes: [ + branches: [ + branchA = StatefulShellBranch(routes: [ GoRoute( path: '/a', builder: _mockScreenBuilder, @@ -495,9 +495,9 @@ void main() { routes: [ StackedShellRoute( builder: mockStackedShellBuilder, - branches: [ + branches: [ branchY = - StackedShellBranch(routes: [ + StatefulShellBranch(routes: [ ShellRoute( builder: _mockShellBuilder, routes: [ @@ -517,7 +517,7 @@ void main() { ], ), ]), - branchB = StackedShellBranch(routes: [ + branchB = StatefulShellBranch(routes: [ ShellRoute( builder: _mockShellBuilder, routes: [ @@ -546,7 +546,7 @@ void main() { }, ); - String? initialLocation(StackedShellBranch branch) { + String? initialLocation(StatefulShellBranch branch) { final GoRoute? route = branch.defaultRoute; return route != null ? config.locationForRoute(route) : null; } diff --git a/packages/go_router/test/delegate_test.dart b/packages/go_router/test/delegate_test.dart index b3afc661a4c0..394622ae96a3 100644 --- a/packages/go_router/test/delegate_test.dart +++ b/packages/go_router/test/delegate_test.dart @@ -33,15 +33,15 @@ Future createGoRouter( return router; } -Future createGoRouterWithStackedShellRoute( +Future createGoRouterWithStatefulShellRoute( WidgetTester tester) async { final GoRouter router = GoRouter( initialLocation: '/', routes: [ GoRoute(path: '/', builder: (_, __) => const DummyStatefulWidget()), GoRoute(path: '/a', builder: (_, __) => const DummyStatefulWidget()), - StackedShellRoute(branches: [ - StackedShellBranch(routes: [ + StackedShellRoute(branches: [ + StatefulShellBranch(routes: [ GoRoute( path: '/c', builder: (_, __) => const DummyStatefulWidget(), @@ -54,7 +54,7 @@ Future createGoRouterWithStackedShellRoute( builder: (_, __) => const DummyStatefulWidget()), ]), ]), - StackedShellBranch(routes: [ + StatefulShellBranch(routes: [ GoRoute( path: '/d', builder: (_, __) => const DummyStatefulWidget(), @@ -125,10 +125,10 @@ void main() { testWidgets( 'It should successfully push a route from outside the the current ' - 'StackedShellRoute', + 'StatefulShellRoute', (WidgetTester tester) async { final GoRouter goRouter = - await createGoRouterWithStackedShellRoute(tester); + await createGoRouterWithStatefulShellRoute(tester); goRouter.push('/c/c1'); await tester.pumpAndSettle(); @@ -145,10 +145,10 @@ void main() { testWidgets( 'It should successfully push a route that is a descendant of the current ' - 'StackedShellRoute branch', + 'StatefulShellRoute branch', (WidgetTester tester) async { final GoRouter goRouter = - await createGoRouterWithStackedShellRoute(tester); + await createGoRouterWithStatefulShellRoute(tester); goRouter.push('/c/c1'); await tester.pumpAndSettle(); @@ -164,11 +164,11 @@ void main() { ); testWidgets( - 'It should successfully push the root of the current StackedShellRoute ' + 'It should successfully push the root of the current StatefulShellRoute ' 'branch upon itself', (WidgetTester tester) async { final GoRouter goRouter = - await createGoRouterWithStackedShellRoute(tester); + await createGoRouterWithStatefulShellRoute(tester); goRouter.push('/c'); await tester.pumpAndSettle(); diff --git a/packages/go_router/test/go_router_test.dart b/packages/go_router/test/go_router_test.dart index c0c015ef14d4..16be4224ef3b 100644 --- a/packages/go_router/test/go_router_test.dart +++ b/packages/go_router/test/go_router_test.dart @@ -2505,16 +2505,16 @@ void main() { testWidgets('StackedShellRoute supports nested routes with params', (WidgetTester tester) async { - StackedNavigationShell? routeState; + StatefulNavigationShell? routeState; final List routes = [ StackedShellRoute( builder: (BuildContext context, GoRouterState state, - StackedNavigationShell navigationShell) { + StatefulNavigationShell navigationShell) { routeState = navigationShell; return navigationShell; }, - branches: [ - StackedShellBranch( + branches: [ + StatefulShellBranch( routes: [ GoRoute( path: '/a', @@ -2523,7 +2523,7 @@ void main() { ), ], ), - StackedShellBranch( + StatefulShellBranch( routes: [ GoRoute( path: '/family', @@ -3081,17 +3081,17 @@ void main() { final List routes = [ StackedShellRoute( builder: (BuildContext context, GoRouterState state, - StackedNavigationShell navigationShell) => + StatefulNavigationShell navigationShell) => navigationShell, - branches: [ - StackedShellBranch(routes: [ + branches: [ + StatefulShellBranch(routes: [ GoRoute( path: '/a', builder: (BuildContext context, GoRouterState state) => const Text('Screen A'), ), ]), - StackedShellBranch(routes: [ + StatefulShellBranch(routes: [ GoRoute( path: '/b', builder: (BuildContext context, GoRouterState state) => @@ -3126,17 +3126,17 @@ void main() { routes: [ StackedShellRoute( builder: (BuildContext context, GoRouterState state, - StackedNavigationShell navigationShell) => + StatefulNavigationShell navigationShell) => navigationShell, - branches: [ - StackedShellBranch(routes: [ + branches: [ + StatefulShellBranch(routes: [ GoRoute( path: 'a', builder: (BuildContext context, GoRouterState state) => const Text('Screen A'), ), ]), - StackedShellBranch(routes: [ + StatefulShellBranch(routes: [ GoRoute( path: 'b', builder: (BuildContext context, GoRouterState state) => @@ -3167,17 +3167,17 @@ void main() { GlobalKey(); final GlobalKey statefulWidgetKey = GlobalKey(); - StackedNavigationShell? routeState; + StatefulNavigationShell? routeState; final List routes = [ StackedShellRoute( builder: (BuildContext context, GoRouterState state, - StackedNavigationShell navigationShell) { + StatefulNavigationShell navigationShell) { routeState = navigationShell; return navigationShell; }, - branches: [ - StackedShellBranch( + branches: [ + StatefulShellBranch( routes: [ GoRoute( path: '/a', @@ -3186,7 +3186,7 @@ void main() { ), ], ), - StackedShellBranch( + StatefulShellBranch( routes: [ GoRoute( path: '/b', @@ -3195,7 +3195,7 @@ void main() { ), ], ), - StackedShellBranch( + StatefulShellBranch( routes: [ GoRoute( path: '/c', @@ -3204,7 +3204,7 @@ void main() { ), ], ), - StackedShellBranch( + StatefulShellBranch( routes: [ GoRoute( path: '/d', @@ -3259,17 +3259,17 @@ void main() { GlobalKey(); final GlobalKey statefulWidgetKey = GlobalKey(); - StackedNavigationShell? routeState; + StatefulNavigationShell? routeState; final List routes = [ StackedShellRoute( builder: (BuildContext context, GoRouterState state, - StackedNavigationShell navigationShell) { + StatefulNavigationShell navigationShell) { routeState = navigationShell; return navigationShell; }, - branches: [ - StackedShellBranch(routes: [ + branches: [ + StatefulShellBranch(routes: [ GoRoute( path: '/a', builder: (BuildContext context, GoRouterState state) => @@ -3286,7 +3286,7 @@ void main() { ], ), ]), - StackedShellBranch(routes: [ + StatefulShellBranch(routes: [ GoRoute( path: '/b', builder: (BuildContext context, GoRouterState state) => @@ -3329,26 +3329,26 @@ void main() { GlobalKey(); final GlobalKey statefulWidgetKey = GlobalKey(); - StackedNavigationShell? routeState1; - StackedNavigationShell? routeState2; + StatefulNavigationShell? routeState1; + StatefulNavigationShell? routeState2; final List routes = [ StackedShellRoute( builder: (BuildContext context, GoRouterState state, - StackedNavigationShell navigationShell) { + StatefulNavigationShell navigationShell) { routeState1 = navigationShell; return navigationShell; }, - branches: [ - StackedShellBranch(routes: [ + branches: [ + StatefulShellBranch(routes: [ StackedShellRoute( builder: (BuildContext context, GoRouterState state, - StackedNavigationShell navigationShell) { + StatefulNavigationShell navigationShell) { routeState2 = navigationShell; return navigationShell; }, - branches: [ - StackedShellBranch(routes: [ + branches: [ + StatefulShellBranch(routes: [ GoRoute( path: '/a', builder: (BuildContext context, GoRouterState state) => @@ -3366,14 +3366,14 @@ void main() { ], ), ]), - StackedShellBranch(routes: [ + StatefulShellBranch(routes: [ GoRoute( path: '/b', builder: (BuildContext context, GoRouterState state) => const Text('Screen B'), ), ]), - StackedShellBranch(routes: [ + StatefulShellBranch(routes: [ GoRoute( path: '/c', builder: (BuildContext context, GoRouterState state) => @@ -3382,7 +3382,7 @@ void main() { ]), ]), ]), - StackedShellBranch(routes: [ + StatefulShellBranch(routes: [ GoRoute( path: '/d', builder: (BuildContext context, GoRouterState state) => @@ -3428,17 +3428,17 @@ void main() { GlobalKey(); final GlobalKey sectionBNavigatorKey = GlobalKey(); - StackedNavigationShell? routeState; + StatefulNavigationShell? routeState; final List routes = [ StackedShellRoute( builder: (BuildContext context, GoRouterState state, - StackedNavigationShell navigationShell) { + StatefulNavigationShell navigationShell) { routeState = navigationShell; return navigationShell; }, - branches: [ - StackedShellBranch( + branches: [ + StatefulShellBranch( navigatorKey: sectionANavigatorKey, routes: [ GoRoute( @@ -3454,7 +3454,7 @@ void main() { ], ), ]), - StackedShellBranch( + StatefulShellBranch( navigatorKey: sectionBNavigatorKey, routes: [ GoRoute( @@ -3513,24 +3513,24 @@ void main() { 'between branches in StackedShellRoute', (WidgetTester tester) async { final GlobalKey rootNavigatorKey = GlobalKey(); - StackedNavigationShell? routeState; + StatefulNavigationShell? routeState; final List routes = [ StackedShellRoute( builder: (BuildContext context, GoRouterState state, - StackedNavigationShell navigationShell) { + StatefulNavigationShell navigationShell) { routeState = navigationShell; return navigationShell; }, - branches: [ - StackedShellBranch(routes: [ + branches: [ + StatefulShellBranch(routes: [ GoRoute( path: '/a', builder: (BuildContext context, GoRouterState state) => const Text('Screen A'), ), ]), - StackedShellBranch(routes: [ + StatefulShellBranch(routes: [ GoRoute( path: '/b', builder: (BuildContext context, GoRouterState state) => @@ -3567,7 +3567,7 @@ void main() { (WidgetTester tester) async { final GlobalKey rootNavigatorKey = GlobalKey(); - StackedNavigationShell? routeState; + StatefulNavigationShell? routeState; final List routes = [ GoRoute( @@ -3577,19 +3577,19 @@ void main() { ), StackedShellRoute( builder: (BuildContext context, GoRouterState state, - StackedNavigationShell navigationShell) { + StatefulNavigationShell navigationShell) { routeState = navigationShell; return navigationShell; }, - branches: [ - StackedShellBranch(routes: [ + branches: [ + StatefulShellBranch(routes: [ GoRoute( path: '/a', builder: (BuildContext context, GoRouterState state) => const Text('Screen A'), ), ]), - StackedShellBranch(routes: [ + StatefulShellBranch(routes: [ GoRoute( path: '/b', builder: (BuildContext context, GoRouterState state) => @@ -3631,24 +3631,24 @@ void main() { 'StackedShellRoute', (WidgetTester tester) async { final GlobalKey rootNavigatorKey = GlobalKey(); - StackedNavigationShell? routeState; + StatefulNavigationShell? routeState; final List routes = [ StackedShellRoute( builder: (BuildContext context, GoRouterState state, - StackedNavigationShell navigationShell) { + StatefulNavigationShell navigationShell) { routeState = navigationShell; return navigationShell; }, - branches: [ - StackedShellBranch(routes: [ + branches: [ + StatefulShellBranch(routes: [ GoRoute( path: '/a', builder: (BuildContext context, GoRouterState state) => const Text('Screen A'), ), ]), - StackedShellBranch(routes: [ + StatefulShellBranch(routes: [ GoRoute( path: '/b', builder: (BuildContext context, GoRouterState state) => @@ -3667,7 +3667,7 @@ void main() { ], ), ]), - StackedShellBranch(routes: [ + StatefulShellBranch(routes: [ GoRoute( path: '/c', redirect: (_, __) => '/c/main2', @@ -3851,8 +3851,8 @@ void main() { routes: [ StackedShellRoute( builder: mockStackedShellBuilder, - branches: [ - StackedShellBranch(routes: [ + branches: [ + StatefulShellBranch(routes: [ GoRoute( path: '/a', builder: (BuildContext context, _) { @@ -3862,7 +3862,7 @@ void main() { }, ), ]), - StackedShellBranch(routes: [ + StatefulShellBranch(routes: [ GoRoute( path: '/b', builder: (BuildContext context, _) { @@ -4257,85 +4257,6 @@ void main() { expect(statefulWidgetKeyA.currentState?.counter, equals(1)); }); - testWidgets('Restores state for imperative routes correctly', - (WidgetTester tester) async { - final GlobalKey statefulWidgetKeyA = - GlobalKey(); - final GlobalKey - statefulWidgetKeyPushed = - GlobalKey(); - - final List routes = [ - GoRoute( - path: '/a', - pageBuilder: createPageBuilder( - restorationId: 'screenA', child: const Text('Screen A')), - routes: [ - GoRoute( - path: 'detail', - pageBuilder: createPageBuilder( - restorationId: 'screenADetail', - child: Column(children: [ - const Text('Screen A Detail'), - DummyRestorableStatefulWidget( - key: statefulWidgetKeyA, restorationId: 'counterA'), - ])), - ), - ], - ), - GoRoute( - path: '/pushed', - pageBuilder: createPageBuilder( - restorationId: 'pushed', - child: Column(children: [ - const Text('Pushed screen'), - DummyRestorableStatefulWidget( - key: statefulWidgetKeyPushed, - restorationId: 'counterPushed'), - ])), - ), - ]; - - final GoRouter router = await createRouter(routes, tester, - initialLocation: '/a/detail', restorationScopeId: 'test'); - await tester.pumpAndSettle(); - statefulWidgetKeyA.currentState?.increment(); - expect(statefulWidgetKeyA.currentState?.counter, equals(1)); - - router.push('/pushed'); - await tester.pumpAndSettle(); - expect(find.text('Pushed screen'), findsOneWidget); - expect(find.text('Screen A Detail'), findsNothing); - statefulWidgetKeyPushed.currentState?.increment(2); - expect(statefulWidgetKeyPushed.currentState?.counter, equals(2)); - await tester.pumpAndSettle(); // Give state change time to persist - - await tester.restartAndRestore(); - - await tester.pumpAndSettle(); - expect(find.text('Pushed screen'), findsOneWidget); - expect(find.text('Screen A Detail'), findsNothing); - expect(statefulWidgetKeyPushed.currentState?.counter, equals(2)); - // Verify that the page key is restored correctly - expect(router.routerDelegate.matches.last.pageKey.value, - equals('/pushed-p0')); - - router.pop(); - await tester.pumpAndSettle(); - expect(find.text('Pushed screen'), findsNothing); - expect(find.text('Screen A Detail'), findsOneWidget); - expect(statefulWidgetKeyA.currentState?.counter, equals(1)); - - router.push('/pushed'); - await tester.pumpAndSettle(); - expect(find.text('Pushed screen'), findsOneWidget); - expect(find.text('Screen A Detail'), findsNothing); - // Verify that the page key is incremented correctly after restore (i.e. - // not starting at 0) - expect(router.routerDelegate.matches.last.pageKey.value, - equals('/pushed-p1')); - }); - testWidgets('Restores state of branches in StackedShellRoute correctly', (WidgetTester tester) async { final GlobalKey rootNavigatorKey = @@ -4346,59 +4267,65 @@ void main() { GlobalKey(); final GlobalKey statefulWidgetKeyC = GlobalKey(); - StackedNavigationShell? routeState; + StatefulNavigationShell? routeState; final List routes = [ StackedShellRoute( restorationScopeId: 'shell', pageBuilder: (BuildContext context, GoRouterState state, - StackedNavigationShell navigationShell) { + StatefulNavigationShell navigationShell) { routeState = navigationShell; return MaterialPage( restorationId: 'shellWidget', child: navigationShell); }, - branches: [ - StackedShellBranch(restorationScopeId: 'branchA', routes: [ - GoRoute( - path: '/a', - pageBuilder: createPageBuilder( - restorationId: 'screenA', child: const Text('Screen A')), - routes: [ + branches: [ + StatefulShellBranch( + restorationScopeId: 'branchA', + routes: [ GoRoute( - path: 'detailA', + path: '/a', pageBuilder: createPageBuilder( - restorationId: 'screenADetail', - child: Column(children: [ - const Text('Screen A Detail'), - DummyRestorableStatefulWidget( - key: statefulWidgetKeyA, - restorationId: 'counterA'), - ])), + restorationId: 'screenA', + child: const Text('Screen A')), + routes: [ + GoRoute( + path: 'detailA', + pageBuilder: createPageBuilder( + restorationId: 'screenADetail', + child: Column(children: [ + const Text('Screen A Detail'), + DummyRestorableStatefulWidget( + key: statefulWidgetKeyA, + restorationId: 'counterA'), + ])), + ), + ], ), - ], - ), - ]), - StackedShellBranch(restorationScopeId: 'branchB', routes: [ - GoRoute( - path: '/b', - pageBuilder: createPageBuilder( - restorationId: 'screenB', child: const Text('Screen B')), - routes: [ + ]), + StatefulShellBranch( + restorationScopeId: 'branchB', + routes: [ GoRoute( - path: 'detailB', + path: '/b', pageBuilder: createPageBuilder( - restorationId: 'screenBDetail', - child: Column(children: [ - const Text('Screen B Detail'), - DummyRestorableStatefulWidget( - key: statefulWidgetKeyB, - restorationId: 'counterB'), - ])), + restorationId: 'screenB', + child: const Text('Screen B')), + routes: [ + GoRoute( + path: 'detailB', + pageBuilder: createPageBuilder( + restorationId: 'screenBDetail', + child: Column(children: [ + const Text('Screen B Detail'), + DummyRestorableStatefulWidget( + key: statefulWidgetKeyB, + restorationId: 'counterB'), + ])), + ), + ], ), - ], - ), - ]), - StackedShellBranch(routes: [ + ]), + StatefulShellBranch(routes: [ GoRoute( path: '/c', pageBuilder: createPageBuilder( @@ -4471,53 +4398,56 @@ void main() { GlobalKey(); final GlobalKey statefulWidgetKeyB = GlobalKey(); - StackedNavigationShell? routeStateRoot; - StackedNavigationShell? routeStateNested; + StatefulNavigationShell? routeStateRoot; + StatefulNavigationShell? routeStateNested; final List routes = [ StackedShellRoute( restorationScopeId: 'shell', pageBuilder: (BuildContext context, GoRouterState state, - StackedNavigationShell navigationShell) { + StatefulNavigationShell navigationShell) { routeStateRoot = navigationShell; return MaterialPage( restorationId: 'shellWidget', child: navigationShell); }, - branches: [ - StackedShellBranch(restorationScopeId: 'branchA', routes: [ - GoRoute( - path: '/a', - pageBuilder: createPageBuilder( - restorationId: 'screenA', child: const Text('Screen A')), - routes: [ + branches: [ + StatefulShellBranch( + restorationScopeId: 'branchA', + routes: [ GoRoute( - path: 'detailA', + path: '/a', pageBuilder: createPageBuilder( - restorationId: 'screenADetail', - child: Column(children: [ - const Text('Screen A Detail'), - DummyRestorableStatefulWidget( - key: statefulWidgetKeyA, - restorationId: 'counterA'), - ])), + restorationId: 'screenA', + child: const Text('Screen A')), + routes: [ + GoRoute( + path: 'detailA', + pageBuilder: createPageBuilder( + restorationId: 'screenADetail', + child: Column(children: [ + const Text('Screen A Detail'), + DummyRestorableStatefulWidget( + key: statefulWidgetKeyA, + restorationId: 'counterA'), + ])), + ), + ], ), - ], - ), - ]), - StackedShellBranch( + ]), + StatefulShellBranch( restorationScopeId: 'branchB', routes: [ StackedShellRoute( restorationScopeId: 'branchB-nested-shell', pageBuilder: (BuildContext context, GoRouterState state, - StackedNavigationShell navigationShell) { + StatefulNavigationShell navigationShell) { routeStateNested = navigationShell; return MaterialPage( restorationId: 'shellWidget-nested', child: navigationShell); }, - branches: [ - StackedShellBranch( + branches: [ + StatefulShellBranch( restorationScopeId: 'branchB-nested', routes: [ GoRoute( @@ -4540,7 +4470,7 @@ void main() { ], ), ]), - StackedShellBranch( + StatefulShellBranch( restorationScopeId: 'branchC-nested', routes: [ GoRoute( diff --git a/packages/go_router/test/test_helpers.dart b/packages/go_router/test/test_helpers.dart index a21d634a4eb1..0bc63a750e88 100644 --- a/packages/go_router/test/test_helpers.dart +++ b/packages/go_router/test/test_helpers.dart @@ -292,8 +292,8 @@ GoRouterPageBuilder createPageBuilder( (BuildContext context, GoRouterState state) => MaterialPage(restorationId: restorationId, child: child); -StackedShellRouteBuilder mockStackedShellBuilder = (BuildContext context, - GoRouterState state, StackedNavigationShell navigationShell) { +StatefulShellRouteBuilder mockStackedShellBuilder = (BuildContext context, + GoRouterState state, StatefulNavigationShell navigationShell) { return navigationShell; }; From abbd01b39d9883eddae602636fafb45aab5fb2b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20Lo=CC=88fstrand?= Date: Mon, 1 May 2023 19:56:32 +0200 Subject: [PATCH 101/112] Removed matchListEquals from RouteMatchList and replaced with the now available standard equality check. Exposed field route in StatefulNavigationShell and StatefulNavigationShellState, for more convenient access to the associated StatefulShellRoute. --- .../example/lib/stateful_shell_route.dart | 4 +++ packages/go_router/lib/src/matching.dart | 14 ---------- packages/go_router/lib/src/route.dart | 27 ++++++++++--------- packages/go_router/test/match_test.dart | 2 +- packages/go_router/test/matching_test.dart | 2 +- 5 files changed, 21 insertions(+), 28 deletions(-) diff --git a/packages/go_router/example/lib/stateful_shell_route.dart b/packages/go_router/example/lib/stateful_shell_route.dart index 804832c50959..48e70dd0336b 100644 --- a/packages/go_router/example/lib/stateful_shell_route.dart +++ b/packages/go_router/example/lib/stateful_shell_route.dart @@ -225,6 +225,10 @@ class ScaffoldWithNavBar extends StatelessWidget { return Scaffold( body: navigationShell, 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'), diff --git a/packages/go_router/lib/src/matching.dart b/packages/go_router/lib/src/matching.dart index 4e2ef827764a..bb2cf008bdde 100644 --- a/packages/go_router/lib/src/matching.dart +++ b/packages/go_router/lib/src/matching.dart @@ -266,20 +266,6 @@ class RouteMatchList { } return null; } - - /// Performs a deep comparison of two match lists by comparing the fields - /// of each object. - /// - /// Note that the == and hashCode functions are not overridden by - /// RouteMatchList because it is mutable. - static bool matchListEquals(RouteMatchList a, RouteMatchList b) { - if (identical(a, b)) { - return true; - } - return listEquals(a.matches, b.matches) && - a.uri == b.uri && - mapEquals(a.pathParameters, b.pathParameters); - } } /// Handles encoding and decoding of [RouteMatchList] objects to a format diff --git a/packages/go_router/lib/src/route.dart b/packages/go_router/lib/src/route.dart index 7c109bd705b7..239998266459 100644 --- a/packages/go_router/lib/src/route.dart +++ b/packages/go_router/lib/src/route.dart @@ -959,7 +959,7 @@ class StatefulNavigationShell extends StatefulWidget { (shellRouteContext.route as StatefulShellRoute)._shellStateKey); /// The ShellRouteContext responsible for building the Navigator for the - /// current [StatefulShellBranch] + /// current [StatefulShellBranch]. final ShellRouteContext shellRouteContext; /// The builder for a custom container for shell route Navigators. @@ -972,6 +972,9 @@ class StatefulNavigationShell extends StatefulWidget { final GoRouter _router; + /// 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]. /// @@ -1046,8 +1049,8 @@ class StatefulNavigationShellState extends State with RestorationMixin { final Map _branchNavigators = {}; - StatefulShellRoute get _route => - widget.shellRouteContext.route as StatefulShellRoute; + /// The associated [StatefulShellRoute]. + StatefulShellRoute get route => widget.route; GoRouter get _router => widget._router; RouteMatcher get _matcher => _router.routeInformationParser.matcher; @@ -1056,7 +1059,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 @@ -1081,10 +1084,10 @@ class StatefulNavigationShellState extends State } RouteMatchList? _matchListForBranch(int index) => - _branchLocations[_route.branches[index]]?.value; + _branchLocations[route.branches[index]]?.value; void _updateCurrentBranchStateFromWidget() { - final StatefulShellBranch branch = _route.branches[widget.currentIndex]; + final StatefulShellBranch branch = route.branches[widget.currentIndex]; final ShellRouteContext shellRouteContext = widget.shellRouteContext; /// Create an clone of the current RouteMatchList, to prevent mutations from @@ -1100,8 +1103,8 @@ class StatefulNavigationShellState extends State _branchNavigators[branch.navigatorKey] != null; /// Only update the Navigator of the route match list has changed - final bool locationChanged = !RouteMatchList.matchListEquals( - previousBranchLocation, currentBranchLocation); + final bool locationChanged = + previousBranchLocation != currentBranchLocation; if (locationChanged || !hasExistingNavigator) { _branchNavigators[branch.navigatorKey] = shellRouteContext .navigatorBuilder(branch.observers, branch.restorationScopeId); @@ -1120,7 +1123,7 @@ class StatefulNavigationShellState extends State /// [StatefulShellRoute]. If the branch has not been visited before, this /// method will navigate to initial location of the branch (see void goBranch(int index) { - assert(index >= 0 && index < _route.branches.length); + assert(index >= 0 && index < route.branches.length); final RouteMatchList? matchlist = _matchListForBranch(index); if (matchlist != null && matchlist.isNotEmpty) { final RouteInformation preParsed = @@ -1140,14 +1143,14 @@ 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 @@ -1158,7 +1161,7 @@ class StatefulNavigationShellState extends State @override Widget build(BuildContext context) { - final List children = _route.branches + final List children = route.branches .map((StatefulShellBranch branch) => _BranchNavigatorProxy( key: ObjectKey(branch), branch: branch, diff --git a/packages/go_router/test/match_test.dart b/packages/go_router/test/match_test.dart index 54f1146ca3b5..ad3451148207 100644 --- a/packages/go_router/test/match_test.dart +++ b/packages/go_router/test/match_test.dart @@ -162,7 +162,7 @@ void main() { final RouteMatchList list2 = list.copy(); - expect(RouteMatchList.matchListEquals(list, list2), true); + expect(list, equals(list2)); }); }); } diff --git a/packages/go_router/test/matching_test.dart b/packages/go_router/test/matching_test.dart index fd1acd2a36c1..8a1d12ab5352 100644 --- a/packages/go_router/test/matching_test.dart +++ b/packages/go_router/test/matching_test.dart @@ -105,6 +105,6 @@ void main() { final RouteMatchList? decoded = codec.decodeMatchList(encoded); expect(decoded, isNotNull); - expect(RouteMatchList.matchListEquals(decoded!, list1), isTrue); + expect(decoded, equals(list1)); }); } From 316cf8c61dfc47dbc2504d48a53ed57f657bbd47 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20Lo=CC=88fstrand?= Date: Thu, 4 May 2023 16:07:11 +0200 Subject: [PATCH 102/112] Removed obsolete copy method from RouteMatchList. --- packages/go_router/lib/src/matching.dart | 10 ---------- packages/go_router/lib/src/route.dart | 5 +---- 2 files changed, 1 insertion(+), 14 deletions(-) diff --git a/packages/go_router/lib/src/matching.dart b/packages/go_router/lib/src/matching.dart index bb2cf008bdde..157411b3ca83 100644 --- a/packages/go_router/lib/src/matching.dart +++ b/packages/go_router/lib/src/matching.dart @@ -60,16 +60,6 @@ class RouteMatchList { required this.pathParameters, }) : fullPath = _generateFullPath(matches); - const RouteMatchList._( - this.matches, this.uri, this.pathParameters, this.fullPath); - - /// Creates a copy of this RouteMatchList that can be modified without - /// affecting the original. - RouteMatchList copy() { - return RouteMatchList._(List.from(matches), uri, - Map.from(pathParameters), fullPath); - } - /// Constructs an empty matches object. static RouteMatchList empty = RouteMatchList( matches: const [], diff --git a/packages/go_router/lib/src/route.dart b/packages/go_router/lib/src/route.dart index 239998266459..846f28e6f020 100644 --- a/packages/go_router/lib/src/route.dart +++ b/packages/go_router/lib/src/route.dart @@ -1089,11 +1089,8 @@ class StatefulNavigationShellState extends State void _updateCurrentBranchStateFromWidget() { final StatefulShellBranch branch = route.branches[widget.currentIndex]; final ShellRouteContext shellRouteContext = widget.shellRouteContext; - - /// Create an clone of the current RouteMatchList, to prevent mutations from - /// affecting the copy saved as the current state for this branch. final RouteMatchList currentBranchLocation = - shellRouteContext.routeMatchList.copy(); + shellRouteContext.routeMatchList; final _RestorableRouteMatchList branchLocation = _branchLocation(branch, false); From 79a0c63389fb6a41039a4d25823a711ca2c8c944 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20Lo=CC=88fstrand?= Date: Thu, 4 May 2023 17:34:36 +0200 Subject: [PATCH 103/112] Updated StatefulNavigationShell to only save the part of the RouteMatchList that relates to the current Navigator and below. Removed obsolete field subRoute from ShellRouteContext. --- packages/go_router/lib/src/builder.dart | 18 ++-- packages/go_router/lib/src/route.dart | 56 ++++++++-- packages/go_router/test/go_router_test.dart | 112 ++++++++++++++++++++ packages/go_router/test/match_test.dart | 29 ----- 4 files changed, 170 insertions(+), 45 deletions(-) diff --git a/packages/go_router/lib/src/builder.dart b/packages/go_router/lib/src/builder.dart index 917523023262..8f6587f55b63 100644 --- a/packages/go_router/lib/src/builder.dart +++ b/packages/go_router/lib/src/builder.dart @@ -153,21 +153,20 @@ class RouteBuilder { } finally { /// Clean up previous cache to prevent memory leak, making sure any nested /// stateful shell routes for the current match list are kept. - final Iterable matchListShellRoutes = matchList - .matches - .map((RouteMatch e) => e.route) - .whereType(); - final Set activeKeys = keyToPage.keys.toSet() - ..addAll(_nestedStatefulNavigatorKeys(matchListShellRoutes)); + ..addAll(_nestedStatefulNavigatorKeys(matchList)); _goHeroCache.removeWhere( (GlobalKey key, _) => !activeKeys.contains(key)); } } - Set> _nestedStatefulNavigatorKeys( - Iterable routes) { - return RouteBase.routesRecursively(routes) + static Set> _nestedStatefulNavigatorKeys( + RouteMatchList matchList) { + final Iterable routes = + matchList.matches.map((RouteMatch e) => e.route); + final Iterable matchListShellRoutes = + routes.whereType(); + return RouteBase.routesRecursively(matchListShellRoutes) .whereType() .expand((StatefulShellRoute e) => e.branches.map((StatefulShellBranch b) => b.navigatorKey)) @@ -256,7 +255,6 @@ class RouteBuilder { // Call the ShellRouteBase to create/update the shell route state final ShellRouteContext shellRouteContext = ShellRouteContext( route: route, - subRoute: subRoute, routerState: state, navigatorKey: shellNavigatorKey, routeMatchList: matchList, diff --git a/packages/go_router/lib/src/route.dart b/packages/go_router/lib/src/route.dart index 846f28e6f020..7e32679bc2f6 100644 --- a/packages/go_router/lib/src/route.dart +++ b/packages/go_router/lib/src/route.dart @@ -7,6 +7,7 @@ import 'package:flutter/widgets.dart'; import 'package:meta/meta.dart'; import '../go_router.dart'; +import 'match.dart'; import 'matching.dart'; import 'path_utils.dart'; import 'typedefs.dart'; @@ -351,6 +352,18 @@ abstract class ShellRouteBase extends RouteBase { /// 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); + + /// Returns the keys for the [Navigator]s associated with this shell route. + Iterable> get _navigatorKeys => + >[]; + + /// Returns all the Navigator keys of this shell route as well as those of any + /// descendant shell routes. + Iterable> _navigatorKeysRecursively() { + return RouteBase.routesRecursively([this]) + .whereType() + .expand((ShellRouteBase e) => e._navigatorKeys); + } } /// Context object used when building the shell and Navigator for a shell route. @@ -358,7 +371,6 @@ class ShellRouteContext { /// Constructs a [ShellRouteContext]. ShellRouteContext({ required this.route, - required this.subRoute, required this.routerState, required this.navigatorKey, required this.routeMatchList, @@ -368,9 +380,6 @@ class ShellRouteContext { /// The associated shell route. final ShellRouteBase route; - /// The matched immediate sub-route of the associated shell route. - final RouteBase subRoute; - /// The current route state associated with [route]. final GoRouterState routerState; @@ -378,7 +387,8 @@ class ShellRouteContext { /// [route]. final GlobalKey navigatorKey; - /// The route match list for the current location. + /// The route match list representing the current location within the + /// associated shell route. final RouteMatchList routeMatchList; /// Function used to build the [Navigator] for the current route. @@ -558,6 +568,10 @@ class ShellRoute extends ShellRouteBase { assert(routes.contains(subRoute)); return navigatorKey; } + + @override + Iterable> get _navigatorKeys => + >[navigatorKey]; } /// A route that displays a UI shell with separate [Navigator]s for its @@ -785,6 +799,10 @@ class StatefulShellRoute extends ShellRouteBase { return branch!.navigatorKey; } + @override + Iterable> get _navigatorKeys => + branches.map((StatefulShellBranch b) => b.navigatorKey); + StatefulNavigationShell _createShell( BuildContext context, ShellRouteContext shellRouteContext) => StatefulNavigationShell( @@ -1086,11 +1104,37 @@ class StatefulNavigationShellState extends State RouteMatchList? _matchListForBranch(int index) => _branchLocations[route.branches[index]]?.value; + /// 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 + /// any other (often top-level) Navigator. + RouteMatchList _scopedMatchList(RouteMatchList matchList) { + final Iterable> validKeys = + route._navigatorKeysRecursively(); + final int index = matchList.matches.indexWhere((RouteMatch e) { + final RouteBase route = e.route; + if (e is ImperativeRouteMatch && route is GoRoute) { + return route.parentNavigatorKey != null && + !validKeys.contains(route.parentNavigatorKey); + } + return false; + }); + if (index > 0) { + final List matches = matchList.matches.sublist(0, index); + return RouteMatchList( + matches: matches, + uri: Uri.parse(matches.last.matchedLocation), + pathParameters: matchList.pathParameters, + ); + } + return matchList; + } + void _updateCurrentBranchStateFromWidget() { final StatefulShellBranch branch = route.branches[widget.currentIndex]; final ShellRouteContext shellRouteContext = widget.shellRouteContext; final RouteMatchList currentBranchLocation = - shellRouteContext.routeMatchList; + _scopedMatchList(shellRouteContext.routeMatchList); final _RestorableRouteMatchList branchLocation = _branchLocation(branch, false); diff --git a/packages/go_router/test/go_router_test.dart b/packages/go_router/test/go_router_test.dart index 220f55515fd6..7f9b8f55d98c 100644 --- a/packages/go_router/test/go_router_test.dart +++ b/packages/go_router/test/go_router_test.dart @@ -3722,6 +3722,118 @@ void main() { expect(find.text('Screen B Detail2'), findsOneWidget); expect(find.text('Screen C2'), findsNothing); }); + + testWidgets( + 'Pushed top-level route is correctly handled by StackedShellRoute', + (WidgetTester tester) async { + final GlobalKey rootNavigatorKey = + GlobalKey(); + final GlobalKey nestedNavigatorKey = + GlobalKey(); + StatefulNavigationShell? routeState; + + final List routes = [ + // First level shell + StackedShellRoute( + builder: (BuildContext context, GoRouterState state, + StatefulNavigationShell navigationShell) { + routeState = navigationShell; + return navigationShell; + }, + branches: [ + StatefulShellBranch(routes: [ + GoRoute( + path: '/a', + builder: (BuildContext context, GoRouterState state) => + const Text('Screen A'), + ), + ]), + StatefulShellBranch(routes: [ + // Second level / nested shell + StackedShellRoute( + builder: (BuildContext context, GoRouterState state, + StatefulNavigationShell navigationShell) => + navigationShell, + branches: [ + StatefulShellBranch(routes: [ + GoRoute( + path: '/b1', + builder: (BuildContext context, GoRouterState state) => + const Text('Screen B1'), + ), + ]), + StatefulShellBranch( + navigatorKey: nestedNavigatorKey, + routes: [ + GoRoute( + path: '/b2', + builder: + (BuildContext context, GoRouterState state) => + const Text('Screen B2'), + ), + GoRoute( + path: '/b2-modal', + // We pass an explicit parentNavigatorKey here, to + // properly test the logic in RouteBuilder, i.e. + // routes with parentNavigatorKeys under the shell + // should not be stripped. + parentNavigatorKey: nestedNavigatorKey, + builder: + (BuildContext context, GoRouterState state) => + const Text('Nested Modal'), + ), + ]), + ], + ), + ]), + ], + ), + GoRoute( + path: '/top-modal', + parentNavigatorKey: rootNavigatorKey, + builder: (BuildContext context, GoRouterState state) => + const Text('Top Modal'), + ), + ]; + + final GoRouter router = await createRouter(routes, tester, + initialLocation: '/a', navigatorKey: rootNavigatorKey); + expect(find.text('Screen A'), findsOneWidget); + + routeState!.goBranch(1); + await tester.pumpAndSettle(); + expect(find.text('Screen B1'), findsOneWidget); + + // Navigate nested (second level) shell to second branch + router.go('/b2'); + await tester.pumpAndSettle(); + expect(find.text('Screen B2'), findsOneWidget); + + // Push route over second branch of nested (second level) shell + router.push('/b2-modal'); + await tester.pumpAndSettle(); + expect(find.text('Nested Modal'), findsOneWidget); + + // Push top-level route while on second branch + router.push('/top-modal'); + await tester.pumpAndSettle(); + expect(find.text('Top Modal'), findsOneWidget); + + // Return to shell and first branch + router.go('/a'); + await tester.pumpAndSettle(); + expect(find.text('Screen A'), findsOneWidget); + + // Switch to second branch, which should only contain 'Nested Modal' + // (in the nested shell) + routeState!.goBranch(1); + await tester.pumpAndSettle(); + expect(find.text('Screen A'), findsNothing); + expect(find.text('Screen B1'), findsNothing); + expect(find.text('Screen B2'), findsNothing); + expect(find.text('Top Modal'), findsNothing); + expect(find.text('Nested Modal'), findsOneWidget); + }); }); group('Imperative navigation', () { diff --git a/packages/go_router/test/match_test.dart b/packages/go_router/test/match_test.dart index ad3451148207..712f379d4d16 100644 --- a/packages/go_router/test/match_test.dart +++ b/packages/go_router/test/match_test.dart @@ -6,7 +6,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:go_router/go_router.dart'; import 'package:go_router/src/match.dart'; -import 'package:go_router/src/matching.dart'; void main() { group('RouteMatch', () { @@ -137,34 +136,6 @@ void main() { expect(match1!.pageKey, match2!.pageKey); }); }); - - group('RouteMatchList', () { - test( - 'UnmodifiableRouteMatchList lists based on the same RouteMatchList are ' - 'equal', () { - final GoRoute route = GoRoute( - path: '/a', - builder: _builder, - ); - final RouteMatch match1 = RouteMatch( - route: route, - matchedLocation: '/a', - extra: null, - error: null, - pageKey: const ValueKey('/a'), - ); - - final RouteMatchList list = RouteMatchList( - matches: [match1], - uri: Uri.parse('/'), - pathParameters: const {}, - ); - - final RouteMatchList list2 = list.copy(); - - expect(list, equals(list2)); - }); - }); } @immutable From 763b136ac16cd5e5351844e3bad648c40e4ad1e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20Lo=CC=88fstrand?= Date: Mon, 8 May 2023 09:22:09 +0200 Subject: [PATCH 104/112] Minor sample code refactoring (documentation and renaming). --- ...t => stateful_shell_state_restoration.dart} | 0 .../example/lib/stateful_shell_route.dart | 18 ++++++++++++------ 2 files changed, 12 insertions(+), 6 deletions(-) rename packages/go_router/example/lib/others/{stacked_shell_state_restoration.dart => stateful_shell_state_restoration.dart} (100%) diff --git a/packages/go_router/example/lib/others/stacked_shell_state_restoration.dart b/packages/go_router/example/lib/others/stateful_shell_state_restoration.dart similarity index 100% rename from packages/go_router/example/lib/others/stacked_shell_state_restoration.dart rename to packages/go_router/example/lib/others/stateful_shell_state_restoration.dart diff --git a/packages/go_router/example/lib/stateful_shell_route.dart b/packages/go_router/example/lib/stateful_shell_route.dart index 48e70dd0336b..c618c131beb8 100644 --- a/packages/go_router/example/lib/stateful_shell_route.dart +++ b/packages/go_router/example/lib/stateful_shell_route.dart @@ -240,15 +240,21 @@ 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) { - // Navigate to the current location of branch at the provided index. If - // tapping the bar item for the current branch, go to the initial location - // instead. - if (index == navigationShell.currentIndex) { + if (index != navigationShell.currentIndex) { + /// 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); + } else { + /// If tapping the bar item for the branch that is already active, go to + /// the initial location instead. The method + /// effectiveInitialBranchLocation can be used to get the (implicitly or + /// explicitly) configured initial location in a convenient way. GoRouter.of(context) .go(navigationShell.effectiveInitialBranchLocation(index)); - } else { - navigationShell.goBranch(index); } } } From 709ee7a0968003055d916cd34c9cb4ec6e4da92c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20L=C3=B6fstrand?= Date: Wed, 10 May 2023 17:55:19 +0200 Subject: [PATCH 105/112] Apply suggestions from code review Review feedback Co-authored-by: chunhtai <47866232+chunhtai@users.noreply.github.com> --- .../stateful_shell_state_restoration.dart | 28 +++++++++---------- packages/go_router/lib/src/builder.dart | 20 +++++++++---- packages/go_router/lib/src/matching.dart | 2 +- 3 files changed, 30 insertions(+), 20 deletions(-) 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 5d7abe19a9fd..de56366fab2d 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 @@ -27,14 +27,14 @@ class RestorableStatefulShellRouteExampleApp extends StatelessWidget { StackedShellRoute( restorationScopeId: 'shell1', branches: [ - /// The route branch for the first tab of the bottom navigation bar. + // The route branch for the first tab of the bottom navigation bar. StatefulShellBranch( navigatorKey: _tabANavigatorKey, restorationScopeId: 'branchA', routes: [ GoRoute( - /// The screen to display as the root in the first tab of the - /// bottom navigation bar. + // The screen to display as the root in the first tab of the + // bottom navigation bar. path: '/a', pageBuilder: (BuildContext context, GoRouterState state) => const MaterialPage( @@ -42,9 +42,9 @@ class RestorableStatefulShellRouteExampleApp extends StatelessWidget { child: RootScreen(label: 'A', detailsPath: '/a/details')), routes: [ - /// The details screen to display stacked on navigator of the - /// first tab. This will cover screen A but not the application - /// shell (bottom navigation bar). + // The details screen to display stacked on navigator of the + // first tab. This will cover screen A but not the application + // shell (bottom navigation bar). GoRoute( path: 'details', pageBuilder: (BuildContext context, GoRouterState state) => @@ -58,15 +58,15 @@ class RestorableStatefulShellRouteExampleApp extends StatelessWidget { ], ), - /// The route branch for the third tab of the bottom navigation bar. + // The route branch for the third tab of the bottom navigation bar. StatefulShellBranch( restorationScopeId: 'branchB', routes: [ StatefulShellRoute( restorationScopeId: 'shell2', - /// This bottom tab uses a nested shell, wrapping sub routes in a - /// top TabBar. + // This bottom tab uses a nested shell, wrapping sub routes in a + // top TabBar. branches: [ StatefulShellBranch( restorationScopeId: 'branchB1', @@ -134,11 +134,11 @@ class RestorableStatefulShellRouteExampleApp extends StatelessWidget { 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 used in the TabBarView. + // Returning a customized container for the branch + // Navigators (i.e. the `List children` argument). + // + // See TabbedRootScreen for more details on how the children + // are used in the TabBarView. TabbedRootScreen( navigationShell: navigationShell, children: children), ), diff --git a/packages/go_router/lib/src/builder.dart b/packages/go_router/lib/src/builder.dart index 8f6587f55b63..8836828966bf 100644 --- a/packages/go_router/lib/src/builder.dart +++ b/packages/go_router/lib/src/builder.dart @@ -16,10 +16,14 @@ import 'pages/material.dart'; import 'route_data.dart'; import 'typedefs.dart'; -/// On pop page callback that includes the associated [RouteMatch]. +/// Signature for a function that takes in a `route` to be popped with +/// the `result` and returns a boolean decision on whether the pop +/// is successful. /// -/// This is a specialized version of [Navigator.onPopPage], used when creating -/// Navigators in [RouteBuilder]. +/// The `match` is the corresponding [RouteMatch] the `route` +/// associates with. +/// +/// Used by of [RouteBuilder.onPopPageWithRouteMatch]. typedef PopPageWithRouteMatchCallback = bool Function( Route route, dynamic result, RouteMatch? match); @@ -56,8 +60,12 @@ class RouteBuilder { /// changes. final List observers; - /// Function used as [Navigator.onPopPage] callback, that additionally - /// provides the [RouteMatch] associated with the popped Page. + /// A callback called when a `route` produced by `match` is about to be popped + /// with the `result`. + /// + /// If this method returns true, this builder pops the `route` and `match`. + /// + /// If this method returns false, this builder aborts the pop. final PopPageWithRouteMatchCallback onPopPageWithRouteMatch; final GoRouterStateRegistry _registry = GoRouterStateRegistry(); @@ -111,6 +119,8 @@ class RouteBuilder { GlobalKey navigatorKey, Map, GoRouterState> registry, ) { + // TODO(chunhtai): move the state from local scope to a central place. + // https://github.com/flutter/flutter/issues/126365 final _PagePopContext pagePopContext = _PagePopContext._(onPopPageWithRouteMatch); return builderWithNav( diff --git a/packages/go_router/lib/src/matching.dart b/packages/go_router/lib/src/matching.dart index 157411b3ca83..8dd3e8be9b8d 100644 --- a/packages/go_router/lib/src/matching.dart +++ b/packages/go_router/lib/src/matching.dart @@ -193,7 +193,7 @@ class RouteMatchList { RouteMatch get last => matches.last; /// Returns true if the current match intends to display an error screen. - bool get isError => matches.length == 1 && matches.first.error != null; + bool get isError => error != null /// Returns the error that this match intends to display. Exception? get error => matches.firstOrNull?.error; From bb7fd9e03a03232fa15ecfe38623208800acdd93 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20Lo=CC=88fstrand?= Date: Thu, 11 May 2023 00:00:48 +0200 Subject: [PATCH 106/112] Implemented review feedback --- .../others/custom_stateful_shell_route.dart | 404 ++++++++++++++++++ .../stateful_shell_state_restoration.dart | 255 ++--------- .../example/lib/stateful_shell_route.dart | 349 +++------------ packages/go_router/lib/go_router.dart | 1 - packages/go_router/lib/src/builder.dart | 113 ++--- packages/go_router/lib/src/configuration.dart | 28 +- packages/go_router/lib/src/matching.dart | 8 +- packages/go_router/lib/src/parser.dart | 33 +- packages/go_router/lib/src/route.dart | 60 +-- packages/go_router/test/builder_test.dart | 2 +- .../go_router/test/configuration_test.dart | 40 +- packages/go_router/test/delegate_test.dart | 2 +- packages/go_router/test/go_router_test.dart | 34 +- 13 files changed, 658 insertions(+), 671 deletions(-) create mode 100644 packages/go_router/example/lib/others/custom_stateful_shell_route.dart 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 new file mode 100644 index 000000000000..f9448c3bf110 --- /dev/null +++ b/packages/go_router/example/lib/others/custom_stateful_shell_route.dart @@ -0,0 +1,404 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:collection/collection.dart'; +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; + +final GlobalKey _rootNavigatorKey = + GlobalKey(debugLabel: 'root'); +final GlobalKey _tabANavigatorKey = + GlobalKey(debugLabel: 'tabANav'); + +// This example demonstrates how to setup nested navigation using a +// BottomNavigationBar, where each bar item uses its own persistent navigator, +// i.e. navigation state is maintained separately for each item. This setup also +// enables deep linking into nested pages. +// +// This example also demonstrates how build a nested shell with a custom +// container for the branch Navigators (in this case a TabBarView). + +void main() { + runApp(NestedTabNavigationExampleApp()); +} + +/// An example demonstrating how to use nested navigators +class NestedTabNavigationExampleApp extends StatelessWidget { + /// Creates a NestedTabNavigationExampleApp + NestedTabNavigationExampleApp({super.key}); + + final GoRouter _router = GoRouter( + navigatorKey: _rootNavigatorKey, + initialLocation: '/a', + routes: [ + StatefulShellRoute.indexedStack( + branches: [ + // The route branch for the first tab of the bottom navigation bar. + StatefulShellBranch( + navigatorKey: _tabANavigatorKey, + routes: [ + GoRoute( + // The screen to display as the root in the first tab of the + // bottom navigation bar. + path: '/a', + builder: (BuildContext context, GoRouterState state) => + const RootScreenA(), + routes: [ + // The details screen to display stacked on navigator of the + // first tab. This will cover screen A but not the application + // shell (bottom navigation bar). + GoRoute( + path: 'details', + builder: (BuildContext context, GoRouterState state) => + const DetailsScreen(label: 'A'), + ), + ], + ), + ], + ), + + // The route branch for the third tab of the bottom navigation bar. + StatefulShellBranch( + // 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', + routes: [ + StatefulShellRoute( + // 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: [ + 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: [ + GoRoute( + path: 'details', + builder: + (BuildContext context, GoRouterState state) => + const DetailsScreen( + label: 'B2', + withScaffold: false, + ), + ), + ], + ), + ]), + ], + builder: (BuildContext context, GoRouterState state, + StatefulNavigationShell navigationShell) { + // This nested StatefulShellRoute demonstrates the use of a + // custom container (TabBarView) 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) => + // Returning a customized container for the branch + // Navigators (i.e. the `List children` argument). + // + // See TabbedRootScreen for more details on how the children + // are used in the TabBarView. + TabbedRootScreen( + navigationShell: navigationShell, children: children), + ), + ], + ), + ], + builder: (BuildContext context, GoRouterState state, + StatefulNavigationShell navigationShell) { + // The builder uses the standard way of building the custom shell for + // a IndexedStack based StatefulShellRoute. + return ScaffoldWithNavBar(navigationShell: navigationShell); + }, + ), + ], + ); + + @override + Widget build(BuildContext context) { + return MaterialApp.router( + title: 'Flutter Demo', + theme: ThemeData( + primarySwatch: Colors.blue, + ), + routerConfig: _router, + ); + } +} + +/// Builds the "shell" for the app by building a Scaffold with a +/// BottomNavigationBar, where [child] is placed in the body of the Scaffold. +class ScaffoldWithNavBar extends StatelessWidget { + /// Constructs an [ScaffoldWithNavBar]. + const ScaffoldWithNavBar({ + required this.navigationShell, + Key? key, + }) : super(key: key ?? const ValueKey('ScaffoldWithNavBar')); + + /// The navigation shell and container for the branch Navigators. + final StatefulNavigationShell navigationShell; + + @override + Widget build(BuildContext context) { + return Scaffold( + body: navigationShell, + 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'), + ], + currentIndex: navigationShell.currentIndex, + onTap: (int index) => _onTap(context, index), + ), + ); + } + + /// 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) { + if (index != navigationShell.currentIndex) { + // 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); + } else { + // If tapping the bar item for the branch that is already active, go to + // the initial location instead. The method + // effectiveInitialBranchLocation can be used to get the (implicitly or + // explicitly) configured initial location in a convenient way. + GoRouter.of(context) + .go(navigationShell.effectiveInitialBranchLocation(index)); + } + } +} + +/// Widget for the root page for the first section of the bottom navigation bar. +class RootScreenA extends StatelessWidget { + /// Creates a RootScreenA + const RootScreenA({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('Root of section A'), + ), + body: Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text('Screen A', style: Theme.of(context).textTheme.titleLarge), + const Padding(padding: EdgeInsets.all(4)), + TextButton( + onPressed: () { + GoRouter.of(context).go('/a/details'); + }, + child: const Text('View details'), + ), + ], + ), + ), + ); + } +} + +/// The details screen for either the A or B screen. +class DetailsScreen extends StatefulWidget { + /// Constructs a [DetailsScreen]. + const DetailsScreen({ + required this.label, + this.param, + this.withScaffold = true, + super.key, + }); + + /// The label to display in the center of the screen. + final String label; + + /// Optional param + final String? param; + + /// Wrap in scaffold + final bool withScaffold; + + @override + State createState() => DetailsScreenState(); +} + +/// The state for DetailsScreen +class DetailsScreenState extends State { + int _counter = 0; + + @override + Widget build(BuildContext context) { + if (widget.withScaffold) { + return Scaffold( + appBar: AppBar( + title: Text('Details Screen - ${widget.label}'), + ), + body: _build(context), + ); + } else { + return Container( + color: Theme.of(context).scaffoldBackgroundColor, + child: _build(context), + ); + } + } + + Widget _build(BuildContext context) { + return Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text('Details for ${widget.label} - Counter: $_counter', + style: Theme.of(context).textTheme.titleLarge), + const Padding(padding: EdgeInsets.all(4)), + TextButton( + onPressed: () { + setState(() { + _counter++; + }); + }, + child: const Text('Increment counter'), + ), + const Padding(padding: EdgeInsets.all(8)), + if (widget.param != null) + Text('Parameter: ${widget.param!}', + style: Theme.of(context).textTheme.titleMedium), + const Padding(padding: EdgeInsets.all(8)), + if (!widget.withScaffold) ...[ + const Padding(padding: EdgeInsets.all(16)), + TextButton( + onPressed: () { + GoRouter.of(context).pop(); + }, + child: const Text('< Back', + style: TextStyle(fontWeight: FontWeight.bold, fontSize: 18)), + ), + ] + ], + ), + ); + } +} + +/// 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}); + + /// The current state of the parent StatefulShellRoute. + final StatefulNavigationShell navigationShell; + + /// The children (Navigators) to display in the [TabBarView]. + final List children; + + @override + State createState() => _TabbedRootScreenState(); +} + +class _TabbedRootScreenState extends State + with SingleTickerProviderStateMixin { + late final TabController _tabController = TabController( + length: widget.children.length, + vsync: this, + initialIndex: widget.navigationShell.currentIndex); + + @override + void didUpdateWidget(covariant TabbedRootScreen oldWidget) { + super.didUpdateWidget(oldWidget); + _tabController.index = widget.navigationShell.currentIndex; + } + + @override + Widget build(BuildContext context) { + final List tabs = widget.children + .mapIndexed((int i, _) => Tab(text: 'Tab ${i + 1}')) + .toList(); + + return Scaffold( + appBar: AppBar( + title: const Text('Root of Section B (nested TabBar shell)'), + bottom: TabBar( + controller: _tabController, + tabs: tabs, + onTap: (int tappedIndex) => _onTabTap(context, tappedIndex), + )), + body: TabBarView( + controller: _tabController, + children: widget.children, + ), + ); + } + + void _onTabTap(BuildContext context, int index) { + widget.navigationShell.goBranch(index); + } +} + +/// Widget for the pages in the top tab bar. +class TabScreen extends StatelessWidget { + /// Creates a RootScreen + const TabScreen({required this.label, required this.detailsPath, super.key}); + + /// The label + final String label; + + /// The path to the detail page + final String detailsPath; + + @override + Widget build(BuildContext context) { + return Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text('Screen $label', style: Theme.of(context).textTheme.titleLarge), + const Padding(padding: EdgeInsets.all(4)), + TextButton( + onPressed: () { + GoRouter.of(context).go(detailsPath); + }, + child: const Text('View details'), + ), + ], + ), + ); + } +} 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 de56366fab2d..d255582fbf5b 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 @@ -2,15 +2,9 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; -final GlobalKey _rootNavigatorKey = - GlobalKey(debugLabel: 'root'); -final GlobalKey _tabANavigatorKey = - GlobalKey(debugLabel: 'tabANav'); - void main() => runApp(RestorableStatefulShellRouteExampleApp()); /// An example demonstrating how to use StatefulShellRoute with state @@ -20,16 +14,14 @@ class RestorableStatefulShellRouteExampleApp extends StatelessWidget { RestorableStatefulShellRouteExampleApp({super.key}); final GoRouter _router = GoRouter( - navigatorKey: _rootNavigatorKey, initialLocation: '/a', restorationScopeId: 'router', routes: [ - StackedShellRoute( + StatefulShellRoute.indexedStack( restorationScopeId: 'shell1', branches: [ // The route branch for the first tab of the bottom navigation bar. StatefulShellBranch( - navigatorKey: _tabANavigatorKey, restorationScopeId: 'branchA', routes: [ GoRoute( @@ -48,99 +40,39 @@ class RestorableStatefulShellRouteExampleApp extends StatelessWidget { GoRoute( path: 'details', pageBuilder: (BuildContext context, GoRouterState state) => - MaterialPage( + const MaterialPage( restorationId: 'screenADetail', - child: - DetailsScreen(label: 'A', extra: state.extra)), + child: DetailsScreen(label: 'A')), ), ], ), ], ), - - // The route branch for the third tab of the bottom navigation bar. + // The route branch for the second tab of the bottom navigation bar. StatefulShellBranch( restorationScopeId: 'branchB', routes: [ - StatefulShellRoute( - restorationScopeId: 'shell2', - - // This bottom tab uses a nested shell, wrapping sub routes in a - // top TabBar. - branches: [ - StatefulShellBranch( - restorationScopeId: 'branchB1', - routes: [ - GoRoute( - path: '/b1', - pageBuilder: - (BuildContext context, GoRouterState state) => - const MaterialPage( - restorationId: 'screenB1', - child: TabScreen( - label: 'B1', - detailsPath: '/b1/details')), - routes: [ - GoRoute( - path: 'details', - pageBuilder: - (BuildContext context, GoRouterState state) => - MaterialPage( - restorationId: 'screenB1Detail', - child: DetailsScreen( - label: 'B1', - extra: state.extra, - withScaffold: false, - )), - ), - ], - ), - ]), - StatefulShellBranch( - restorationScopeId: 'branchB2', - routes: [ - GoRoute( - path: '/b2', - pageBuilder: - (BuildContext context, GoRouterState state) => - const MaterialPage( - restorationId: 'screenB2', - child: TabScreen( - label: 'B2', - detailsPath: '/b2/details')), - routes: [ - GoRoute( - path: 'details', - pageBuilder: - (BuildContext context, GoRouterState state) => - MaterialPage( - restorationId: 'screenB2Detail', - child: DetailsScreen( - label: 'B2', - extra: state.extra, - withScaffold: false, - )), - ), - ], - ), - ]), + GoRoute( + // The screen to display as the root in the second tab of the + // bottom navigation bar. + path: '/b', + pageBuilder: (BuildContext context, GoRouterState state) => + const MaterialPage( + restorationId: 'screenB', + child: + RootScreen(label: 'B', detailsPath: '/b/details')), + routes: [ + // The details screen to display stacked on navigator of the + // first tab. This will cover screen A but not the application + // shell (bottom navigation bar). + GoRoute( + path: 'details', + pageBuilder: (BuildContext context, GoRouterState state) => + const MaterialPage( + restorationId: 'screenBDetail', + child: DetailsScreen(label: 'B')), + ), ], - pageBuilder: (BuildContext context, GoRouterState state, - StatefulNavigationShell navigationShell) { - return MaterialPage( - restorationId: 'shellWidget2', child: 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 used in the TabBarView. - TabbedRootScreen( - navigationShell: navigationShell, children: children), ), ], ), @@ -215,7 +147,7 @@ class RootScreen extends StatelessWidget { Widget build(BuildContext context) { return Scaffold( appBar: AppBar( - title: Text('Tab root - $label'), + title: Text('Root of section $label'), ), body: Center( child: Column( @@ -226,7 +158,7 @@ class RootScreen extends StatelessWidget { const Padding(padding: EdgeInsets.all(4)), TextButton( onPressed: () { - GoRouter.of(context).go(detailsPath, extra: '$label-XYZ'); + GoRouter.of(context).go(detailsPath); }, child: const Text('View details'), ), @@ -242,24 +174,12 @@ class DetailsScreen extends StatefulWidget { /// Constructs a [DetailsScreen]. const DetailsScreen({ required this.label, - this.param, - this.extra, - this.withScaffold = true, super.key, }); /// The label to display in the center of the screen. final String label; - /// Optional param - final String? param; - - /// Optional extra object - final Object? extra; - - /// Wrap in scaffold - final bool withScaffold; - @override State createState() => DetailsScreenState(); } @@ -284,19 +204,12 @@ class DetailsScreenState extends State with RestorationMixin { @override Widget build(BuildContext context) { - if (widget.withScaffold) { - return Scaffold( - appBar: AppBar( - title: Text('Details Screen - ${widget.label}'), - ), - body: _build(context), - ); - } else { - return Container( - color: Theme.of(context).scaffoldBackgroundColor, - child: _build(context), - ); - } + return Scaffold( + appBar: AppBar( + title: Text('Details Screen - ${widget.label}'), + ), + body: _build(context), + ); } Widget _build(BuildContext context) { @@ -316,110 +229,6 @@ class DetailsScreenState extends State with RestorationMixin { child: const Text('Increment counter'), ), const Padding(padding: EdgeInsets.all(8)), - if (widget.param != null) - Text('Parameter: ${widget.param!}', - style: Theme.of(context).textTheme.titleMedium), - const Padding(padding: EdgeInsets.all(8)), - if (widget.extra != null) - Text('Extra: ${widget.extra!}', - style: Theme.of(context).textTheme.titleMedium), - if (!widget.withScaffold) ...[ - const Padding(padding: EdgeInsets.all(16)), - TextButton( - onPressed: () { - GoRouter.of(context).pop(); - }, - child: const Text('< Back', - style: TextStyle(fontWeight: FontWeight.bold, fontSize: 18)), - ), - ] - ], - ), - ); - } -} - -/// 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}); - - /// The current state of the parent StatefulShellRoute. - final StatefulNavigationShell navigationShell; - - /// The children (Navigators) to display in the [TabBarView]. - final List children; - - @override - State createState() => _TabbedRootScreenState(); -} - -class _TabbedRootScreenState extends State - with SingleTickerProviderStateMixin { - late final TabController _tabController = TabController( - length: widget.children.length, - vsync: this, - initialIndex: widget.navigationShell.currentIndex); - - @override - void didUpdateWidget(covariant TabbedRootScreen oldWidget) { - super.didUpdateWidget(oldWidget); - _tabController.index = widget.navigationShell.currentIndex; - } - - @override - Widget build(BuildContext context) { - final List tabs = widget.children - .mapIndexed((int i, _) => Tab(text: 'Tab ${i + 1}')) - .toList(); - - return Scaffold( - appBar: AppBar( - title: const Text('Tab root'), - bottom: TabBar( - controller: _tabController, - tabs: tabs, - onTap: (int tappedIndex) => _onTabTap(context, tappedIndex), - )), - body: TabBarView( - controller: _tabController, - children: widget.children, - ), - ); - } - - void _onTabTap(BuildContext context, int index) { - widget.navigationShell.goBranch(index); - } -} - -/// Widget for the pages in the top tab bar. -class TabScreen extends StatelessWidget { - /// Creates a RootScreen - const TabScreen({required this.label, this.detailsPath, super.key}); - - /// The label - final String label; - - /// The path to the detail page - final String? detailsPath; - - @override - Widget build(BuildContext context) { - return Center( - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Text('Screen $label', style: Theme.of(context).textTheme.titleLarge), - const Padding(padding: EdgeInsets.all(4)), - if (detailsPath != null) - TextButton( - onPressed: () { - GoRouter.of(context).go(detailsPath!); - }, - child: const Text('View details'), - ), ], ), ); diff --git a/packages/go_router/example/lib/stateful_shell_route.dart b/packages/go_router/example/lib/stateful_shell_route.dart index c618c131beb8..7e9b0b92eeb7 100644 --- a/packages/go_router/example/lib/stateful_shell_route.dart +++ b/packages/go_router/example/lib/stateful_shell_route.dart @@ -2,24 +2,23 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; final GlobalKey _rootNavigatorKey = GlobalKey(debugLabel: 'root'); -final GlobalKey _tabANavigatorKey = - GlobalKey(debugLabel: 'tabANav'); +final GlobalKey _sectionANavigatorKey = + GlobalKey(debugLabel: 'sectionANav'); // This example demonstrates how to setup nested navigation using a -// BottomNavigationBar, where each tab uses its own persistent navigator, i.e. -// navigation state is maintained separately for each tab. This setup also +// BottomNavigationBar, where each bar item uses its own persistent navigator, +// i.e. navigation state is maintained separately for each item. This setup also // enables deep linking into nested pages. // // This example demonstrates how to display routes within a StatefulShellRoute, // that are places on separate navigators. The example also demonstrates how -// state is maintained when switching between different tabs (and thus branches -// and Navigators). +// state is maintained when switching between different bar items (and thus +// branches and Navigators). void main() { runApp(NestedTabNavigationExampleApp()); @@ -34,47 +33,40 @@ class NestedTabNavigationExampleApp extends StatelessWidget { navigatorKey: _rootNavigatorKey, initialLocation: '/a', routes: [ - GoRoute( - path: '/modal', - parentNavigatorKey: _rootNavigatorKey, - builder: (BuildContext context, GoRouterState state) => - const ModalScreen(), - ), - StackedShellRoute( + StatefulShellRoute.indexedStack( branches: [ - /// The route branch for the first tab of the bottom navigation bar. + // The route branch for the first tab of the bottom navigation bar. StatefulShellBranch( - navigatorKey: _tabANavigatorKey, + navigatorKey: _sectionANavigatorKey, routes: [ GoRoute( - /// The screen to display as the root in the first tab of the - /// bottom navigation bar. + // The screen to display as the root in the first tab of the + // bottom navigation bar. path: '/a', builder: (BuildContext context, GoRouterState state) => const RootScreen(label: 'A', detailsPath: '/a/details'), routes: [ - /// The details screen to display stacked on navigator of the - /// first tab. This will cover screen A but not the application - /// shell (bottom navigation bar). + // The details screen to display stacked on navigator of the + // first tab. This will cover screen A but not the application + // shell (bottom navigation bar). GoRoute( path: 'details', builder: (BuildContext context, GoRouterState state) => - DetailsScreen(label: 'A', extra: state.extra), + const DetailsScreen(label: 'A'), ), ], ), ], ), - /// The route branch for the second tab of the bottom navigation bar. + // The route branch for the second tab of the bottom navigation bar. StatefulShellBranch( - /// It's not necessary to provide a navigatorKey if it isn't also - /// needed elsewhere. If not provided, a default key will be used. - // navigatorKey: _tabBNavigatorKey, + // It's not necessary to provide a navigatorKey if it isn't also + // needed elsewhere. If not provided, a default key will be used. routes: [ GoRoute( - /// The screen to display as the root in the second tab of the - /// bottom navigation bar. + // The screen to display as the root in the second tab of the + // bottom navigation bar. path: '/b', builder: (BuildContext context, GoRouterState state) => const RootScreen( @@ -89,7 +81,6 @@ class NestedTabNavigationExampleApp extends StatelessWidget { DetailsScreen( label: 'B', param: state.pathParameters['param'], - extra: state.extra, ), ), ], @@ -97,96 +88,43 @@ class NestedTabNavigationExampleApp extends StatelessWidget { ], ), - /// The route branch for the third tab of the bottom navigation bar. + // The route branch for the third tab of the bottom navigation bar. StatefulShellBranch( - /// 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', routes: [ - StatefulShellRoute( - /// This bottom tab uses a nested shell, wrapping sub routes in a - /// top TabBar. - branches: [ - StatefulShellBranch(routes: [ - GoRoute( - path: '/c1', - builder: (BuildContext context, GoRouterState state) => - const TabScreen( - label: 'C1', detailsPath: '/c1/details'), - routes: [ - GoRoute( - path: 'details', - builder: - (BuildContext context, GoRouterState state) => - DetailsScreen( - label: 'C1', - extra: state.extra, - withScaffold: false, - ), - ), - ], - ), - ]), - StatefulShellBranch(routes: [ - GoRoute( - path: '/c2', - builder: (BuildContext context, GoRouterState state) => - const TabScreen( - label: 'C2', detailsPath: '/c2/details'), - routes: [ - GoRoute( - path: 'details', - builder: - (BuildContext context, GoRouterState state) => - DetailsScreen( - label: 'C2', - extra: state.extra, - withScaffold: false, - ), - ), - ], + GoRoute( + // The screen to display as the root in the third tab of the + // bottom navigation bar. + path: '/c', + builder: (BuildContext context, GoRouterState state) => + const RootScreen( + label: 'C', + detailsPath: '/c/details', + ), + routes: [ + GoRoute( + path: 'details', + builder: (BuildContext context, GoRouterState state) => + DetailsScreen( + label: 'C', + extra: state.extra, ), - ]), + ), ], - builder: (BuildContext context, GoRouterState state, - StatefulNavigationShell navigationShell) { - /// This nested StatefulShellRoute demonstrates the use of a - /// custom container (TabBarView) 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) => - - /// Returning a customized container for the branch - /// Navigators (i.e. the `List children` argument). - /// - /// See TabbedRootScreen for more details on how the children - /// are used in the TabBarView. - TabbedRootScreen( - navigationShell: navigationShell, children: children), ), ], ), ], builder: (BuildContext context, GoRouterState state, StatefulNavigationShell navigationShell) { - /// This builder implementation uses the default container for the - /// branch Navigators (provided in through the `child` argument). This - /// is the simplest way to use StatefulShellRoute, where the shell is - /// built around the Navigator container (see ScaffoldWithNavBar). + // 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); }, - /// If it's necessary to customize the Page for StatefulShellRoute, - /// provide a pageBuilder function instead of the builder, for example: + // If it's necessary to customize the Page for StatefulShellRoute, + // provide a pageBuilder function instead of the builder, for example: // pageBuilder: (BuildContext context, GoRouterState state, // StatefulNavigationShell navigationShell) { // return NoTransitionPage( @@ -225,10 +163,10 @@ class ScaffoldWithNavBar extends StatelessWidget { return Scaffold( body: navigationShell, 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`. + // 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'), @@ -244,15 +182,15 @@ class ScaffoldWithNavBar extends StatelessWidget { /// tapping an item in the BottomNavigationBar. void _onTap(BuildContext context, int index) { if (index != navigationShell.currentIndex) { - /// 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. + // 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); } else { - /// If tapping the bar item for the branch that is already active, go to - /// the initial location instead. The method - /// effectiveInitialBranchLocation can be used to get the (implicitly or - /// explicitly) configured initial location in a convenient way. + // If tapping the bar item for the branch that is already active, go to + // the initial location instead. The method + // effectiveInitialBranchLocation can be used to get the (implicitly or + // explicitly) configured initial location in a convenient way. GoRouter.of(context) .go(navigationShell.effectiveInitialBranchLocation(index)); } @@ -262,11 +200,12 @@ class ScaffoldWithNavBar extends StatelessWidget { /// Widget for the root/initial pages in the bottom navigation bar. class RootScreen extends StatelessWidget { /// Creates a RootScreen - const RootScreen( - {required this.label, - required this.detailsPath, - this.secondDetailsPath, - super.key}); + const RootScreen({ + required this.label, + required this.detailsPath, + this.secondDetailsPath, + super.key, + }); /// The label final String label; @@ -281,7 +220,7 @@ class RootScreen extends StatelessWidget { Widget build(BuildContext context) { return Scaffold( appBar: AppBar( - title: Text('Tab root - $label'), + title: Text('Root of section $label'), ), body: Center( child: Column( @@ -304,53 +243,6 @@ class RootScreen extends StatelessWidget { }, child: const Text('View more details'), ), - const Padding(padding: EdgeInsets.all(8)), - ElevatedButton( - onPressed: () { - GoRouter.of(context).push('/modal'); - }, - child: const Text('Show modal screen on ROOT navigator'), - ), - const Padding(padding: EdgeInsets.all(4)), - ElevatedButton( - onPressed: () { - showModalBottomSheet( - context: context, - useRootNavigator: true, - builder: _bottomSheet); - }, - child: const Text('Show bottom sheet on ROOT navigator'), - ), - const Padding(padding: EdgeInsets.all(4)), - ElevatedButton( - onPressed: () { - showModalBottomSheet( - context: context, - useRootNavigator: false, - builder: _bottomSheet); - }, - child: const Text('Show bottom sheet on CURRENT navigator'), - ), - ], - ), - ), - ); - } - - Widget _bottomSheet(BuildContext context) { - return Container( - height: 200, - color: Colors.amber, - child: Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - mainAxisSize: MainAxisSize.min, - children: [ - const Text('Modal BottomSheet'), - ElevatedButton( - child: const Text('Close BottomSheet'), - onPressed: () => Navigator.pop(context), - ), ], ), ), @@ -445,120 +337,3 @@ class DetailsScreenState extends State { ); } } - -/// Widget for a modal screen. -class ModalScreen extends StatelessWidget { - /// Creates a ModalScreen - const ModalScreen({super.key}); - - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar( - title: const Text('Modal'), - ), - body: Center( - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Text('Modal screen', style: Theme.of(context).textTheme.titleLarge), - const Padding(padding: EdgeInsets.all(8)), - ElevatedButton( - onPressed: () { - GoRouter.of(context).go('/a'); - }, - child: const Text('Go to initial section'), - ), - ], - ), - ), - ); - } -} - -/// 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}); - - /// The current state of the parent StatefulShellRoute. - final StatefulNavigationShell navigationShell; - - /// The children (Navigators) to display in the [TabBarView]. - final List children; - - @override - State createState() => _TabbedRootScreenState(); -} - -class _TabbedRootScreenState extends State - with SingleTickerProviderStateMixin { - late final TabController _tabController = TabController( - length: widget.children.length, - vsync: this, - initialIndex: widget.navigationShell.currentIndex); - - @override - void didUpdateWidget(covariant TabbedRootScreen oldWidget) { - super.didUpdateWidget(oldWidget); - _tabController.index = widget.navigationShell.currentIndex; - } - - @override - Widget build(BuildContext context) { - final List tabs = widget.children - .mapIndexed((int i, _) => Tab(text: 'Tab ${i + 1}')) - .toList(); - - return Scaffold( - appBar: AppBar( - title: const Text('Tab root'), - bottom: TabBar( - controller: _tabController, - tabs: tabs, - onTap: (int tappedIndex) => _onTabTap(context, tappedIndex), - )), - body: TabBarView( - controller: _tabController, - children: widget.children, - ), - ); - } - - void _onTabTap(BuildContext context, int index) { - widget.navigationShell.goBranch(index); - } -} - -/// Widget for the pages in the top tab bar. -class TabScreen extends StatelessWidget { - /// Creates a RootScreen - const TabScreen({required this.label, this.detailsPath, super.key}); - - /// The label - final String label; - - /// The path to the detail page - final String? detailsPath; - - @override - Widget build(BuildContext context) { - return Center( - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Text('Screen $label', style: Theme.of(context).textTheme.titleLarge), - const Padding(padding: EdgeInsets.all(4)), - if (detailsPath != null) - TextButton( - onPressed: () { - GoRouter.of(context).go(detailsPath!); - }, - child: const Text('View details'), - ), - ], - ), - ); - } -} diff --git a/packages/go_router/lib/go_router.dart b/packages/go_router/lib/go_router.dart index c638b21ed51b..31ed9c865b13 100644 --- a/packages/go_router/lib/go_router.dart +++ b/packages/go_router/lib/go_router.dart @@ -13,7 +13,6 @@ export 'src/configuration.dart' RouteBase, ShellRoute, ShellNavigationContainerBuilder, - StackedShellRoute, StatefulNavigationShell, StatefulNavigationShellState, StatefulShellBranch, diff --git a/packages/go_router/lib/src/builder.dart b/packages/go_router/lib/src/builder.dart index 8836828966bf..37b2ede44eb9 100644 --- a/packages/go_router/lib/src/builder.dart +++ b/packages/go_router/lib/src/builder.dart @@ -22,7 +22,7 @@ import 'typedefs.dart'; /// /// The `match` is the corresponding [RouteMatch] the `route` /// associates with. -/// +/// /// Used by of [RouteBuilder.onPopPageWithRouteMatch]. typedef PopPageWithRouteMatchCallback = bool Function( Route route, dynamic result, RouteMatch? match); @@ -72,6 +72,8 @@ class RouteBuilder { /// Caches a HeroController for the nested Navigator, which solves cases where the /// Hero Widget animation stops working when navigating. + // TODO(chunhtai): Remove _goHeroCache once below issue is fixed: + // https://github.com/flutter/flutter/issues/54200 final Map, HeroController> _goHeroCache = , HeroController>{}; @@ -127,7 +129,7 @@ class RouteBuilder { context, _buildNavigator( pagePopContext.onPopPage, - _buildPages(context, matchList, 0, pagePopContext, routerNeglect, + _buildPages(context, matchList, pagePopContext, routerNeglect, navigatorKey, registry), navigatorKey, observers: observers, @@ -141,7 +143,6 @@ class RouteBuilder { List> _buildPages( BuildContext context, RouteMatchList matchList, - int startIndex, _PagePopContext pagePopContext, bool routerNeglect, GlobalKey navigatorKey, @@ -149,8 +150,8 @@ class RouteBuilder { final Map, List>> keyToPage = , List>>{}; try { - _buildRecursive(context, matchList, startIndex, pagePopContext, - routerNeglect, keyToPage, navigatorKey, registry); + _buildRecursive(context, matchList, 0, pagePopContext, routerNeglect, + keyToPage, navigatorKey, registry); // Every Page should have a corresponding RouteMatch. assert(keyToPage.values.flattened.every((Page page) => @@ -172,11 +173,12 @@ class RouteBuilder { static Set> _nestedStatefulNavigatorKeys( RouteMatchList matchList) { - final Iterable routes = - matchList.matches.map((RouteMatch e) => e.route); - final Iterable matchListShellRoutes = - routes.whereType(); - return RouteBase.routesRecursively(matchListShellRoutes) + final StatefulShellRoute? shellRoute = + matchList.routes.whereType().firstOrNull; + if (shellRoute == null) { + return >{}; + } + return RouteBase.routesRecursively([shellRoute]) .whereType() .expand((StatefulShellRoute e) => e.branches.map((StatefulShellBranch b) => b.navigatorKey)) @@ -205,10 +207,9 @@ class RouteBuilder { final RouteBase route = match.route; final GoRouterState state = buildState(matchList, match); + Page? page; if (route is GoRoute) { - final Page page = - _buildPageForRoute(context, state, match, pagePopContext); - registry[page] = state; + page = _buildPageForGoRoute(context, state, match, route, pagePopContext); // If this GoRoute is for a different Navigator, add it to the // list of out of scope pages final GlobalKey goRouteNavKey = @@ -256,7 +257,7 @@ class RouteBuilder { pagePopContext.onPopPage, keyToPages[shellNavigatorKey]!, shellNavigatorKey, - observers: observers, + observers: observers ?? const [], restorationScopeId: restorationScopeId, heroController: heroController, ); @@ -272,22 +273,26 @@ class RouteBuilder { ); // Build the Page for this route - final Page page = _buildPageForRoute( - context, state, match, pagePopContext, - shellRouteContext: shellRouteContext); - registry[page] = state; + page = _buildPageForShellRoute( + context, state, match, route, pagePopContext, shellRouteContext); // Place the ShellRoute's Page onto the list for the parent navigator. keyToPages .putIfAbsent(parentNavigatorKey, () => >[]) .insert(shellPageIdx, page); } + if (page != null) { + registry[page] = state; + pagePopContext._setRouteMatchForPage(page, match); + } else { + throw _RouteBuilderException('Unsupported route type $route'); + } } static Widget _buildNavigator( PopPageCallback onPopPage, List> pages, Key? navigatorKey, { - List? observers, + List observers = const [], String? restorationScopeId, HeroController? heroController, }) { @@ -295,7 +300,7 @@ class RouteBuilder { key: navigatorKey, restorationScopeId: restorationScopeId, pages: pages, - observers: observers ?? const [], + observers: observers, onPopPage: onPopPage, ); if (heroController != null) { @@ -321,7 +326,7 @@ class RouteBuilder { } final RouteMatchList effectiveMatchList = match is ImperativeRouteMatch ? match.matches : matchList; - final GoRouterState state = GoRouterState( + return GoRouterState( configuration, location: effectiveMatchList.uri.toString(), matchedLocation: match.matchedLocation, @@ -336,46 +341,27 @@ class RouteBuilder { extra: match.extra, pageKey: match.pageKey, ); - return state; } - /// Builds a [Page] for [StackedRoute] - Page _buildPageForRoute(BuildContext context, GoRouterState state, - RouteMatch match, _PagePopContext pagePopContext, - {ShellRouteContext? shellRouteContext}) { - final RouteBase route = match.route; + /// Builds a [Page] for [GoRoute] + Page _buildPageForGoRoute(BuildContext context, GoRouterState state, + RouteMatch match, GoRoute route, _PagePopContext pagePopContext) { Page? page; - if (route is GoRoute) { - // Call the pageBuilder if it's non-null - final GoRouterPageBuilder? pageBuilder = route.pageBuilder; - if (pageBuilder != null) { - page = pageBuilder(context, state); + // Call the pageBuilder if it's non-null + final GoRouterPageBuilder? pageBuilder = route.pageBuilder; + if (pageBuilder != null) { + page = pageBuilder(context, state); + if (page is NoOpPage) { + page = null; } - } else if (route is ShellRouteBase) { - assert(shellRouteContext != null, - 'ShellRouteContext must be provided for ${route.runtimeType}'); - page = route.buildPage(context, state, shellRouteContext!); } - if (page is NoOpPage) { - page = null; - } - - page ??= buildPage(context, state, Builder(builder: (BuildContext context) { - final RouteBase route = match.route; - if (route is GoRoute) { - return _callGoRouteBuilder(context, state, route); - } else if (route is ShellRouteBase) { - return _callShellRouteBaseBuilder( - context, state, route, shellRouteContext); - } - throw _RouteBuilderException('Unsupported route type $route'); - })); - pagePopContext._setRouteMatchForPage(page, match); - // Return the result of the route's builder() or pageBuilder() - return page; + return page ?? + buildPage(context, state, Builder(builder: (BuildContext context) { + return _callGoRouteBuilder(context, state, route); + })); } /// Calls the user-provided route builder from the [GoRoute]. @@ -390,6 +376,27 @@ class RouteBuilder { return builder(context, state); } + /// Builds a [Page] for [ShellRouteBase] + Page _buildPageForShellRoute( + BuildContext context, + GoRouterState state, + RouteMatch match, + ShellRouteBase route, + _PagePopContext pagePopContext, + ShellRouteContext shellRouteContext) { + Page? page = route.buildPage(context, state, shellRouteContext); + if (page is NoOpPage) { + page = null; + } + + // Return the result of the route's builder() or pageBuilder() + return page ?? + buildPage(context, state, Builder(builder: (BuildContext context) { + return _callShellRouteBaseBuilder( + context, state, route, shellRouteContext); + })); + } + /// Calls the user-provided route builder from the [ShellRouteBase]. Widget _callShellRouteBaseBuilder(BuildContext context, GoRouterState state, ShellRouteBase route, ShellRouteContext? shellRouteContext) { diff --git a/packages/go_router/lib/src/configuration.dart b/packages/go_router/lib/src/configuration.dart index b574bd8f2ac1..07d02d469c3c 100644 --- a/packages/go_router/lib/src/configuration.dart +++ b/packages/go_router/lib/src/configuration.dart @@ -2,7 +2,6 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'package:collection/collection.dart'; import 'package:flutter/widgets.dart'; import 'configuration.dart'; @@ -146,16 +145,23 @@ class RouteConfiguration { route != null ? locationForRoute(route) : null; assert( initialLocation != null, - 'The initial location of a StatefulShellBranch must be ' + '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 RouteBase initialLocationRoute = - matcher.findMatch(branch.initialLocation!).last.route; - final RouteBase? match = branch.routes.firstWhereOrNull( - (RouteBase e) => _debugIsDescendantOrSame( - ancestor: e, route: initialLocationRoute)); + final List matchRoutes = + matcher.findMatch(branch.initialLocation!).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( - match != null, + matchFound, 'The initialLocation (${branch.initialLocation}) of ' 'StatefulShellBranch must match a descendant route of the ' 'branch'); @@ -173,12 +179,6 @@ class RouteConfiguration { return true; } - /// Tests if a route is a descendant of, or same as, an ancestor route. - bool _debugIsDescendantOrSame( - {required RouteBase ancestor, required RouteBase route}) => - ancestor == route || - RouteBase.routesRecursively(ancestor.routes).contains(route); - /// The list of top level routes used by [GoRouterDelegate]. final List routes; diff --git a/packages/go_router/lib/src/matching.dart b/packages/go_router/lib/src/matching.dart index 8dd3e8be9b8d..e295b526f94d 100644 --- a/packages/go_router/lib/src/matching.dart +++ b/packages/go_router/lib/src/matching.dart @@ -193,11 +193,14 @@ class RouteMatchList { RouteMatch get last => matches.last; /// Returns true if the current match intends to display an error screen. - bool get isError => error != null + bool get isError => error != null; /// Returns the error that this match intends to display. Exception? get error => matches.firstOrNull?.error; + /// The routes for each of the matches. + List get routes => matches.map((RouteMatch e) => e.route).toList(); + RouteMatchList _copyWith({ List? matches, Uri? uri, @@ -241,6 +244,9 @@ class RouteMatchList { /// Returns a pre-parsed [RouteInformation], containing a reference to this /// match list. RouteInformation toPreParsedRouteInformation() { + // TODO(tolo): remove this ignore and migrate the code + // https://github.com/flutter/flutter/issues/124045. + // ignore: deprecated_member_use return RouteInformation( location: uri.toString(), state: this, diff --git a/packages/go_router/lib/src/parser.dart b/packages/go_router/lib/src/parser.dart index 81e15bcaf37c..78a7ee0624ec 100644 --- a/packages/go_router/lib/src/parser.dart +++ b/packages/go_router/lib/src/parser.dart @@ -31,8 +31,6 @@ class GoRouteInformationParser extends RouteInformationParser { /// The route matcher. final RouteMatcher matcher; - late final RouteMatchListCodec _matchListCodec = RouteMatchListCodec(matcher); - /// The route redirector. final RouteRedirector redirector; @@ -59,19 +57,16 @@ class GoRouteInformationParser extends RouteInformationParser { ) { late final RouteMatchList initialMatches; try { - // TODO(chunhtai): remove this ignore and migrate the code - // https://github.com/flutter/flutter/issues/124045. - // ignore: deprecated_member_use, unnecessary_non_null_assertion final RouteMatchList? preParsedMatchList = RouteMatchList.fromPreParsedRouteInformation(routeInformation); if (preParsedMatchList != null) { initialMatches = preParsedMatchList; } else { - final RouteMatchList? decodedMatchList = - _matchListCodec.decodeMatchList(routeInformation.state); - initialMatches = decodedMatchList ?? - matcher.findMatch(routeInformation.location!, - extra: routeInformation.state); + // TODO(chunhtai): remove this ignore and migrate the code + // https://github.com/flutter/flutter/issues/124045. + // ignore: deprecated_member_use, unnecessary_non_null_assertion + initialMatches = matcher.findMatch(routeInformation.location!, + extra: routeInformation.state); } } on MatcherError { // TODO(chunhtai): remove this ignore and migrate the code @@ -90,18 +85,6 @@ class GoRouteInformationParser extends RouteInformationParser { pathParameters: const {}, ); } - return processRedirection(initialMatches, context, - topRouteInformation: routeInformation); - } - - /// Processes any redirections for the provided RouteMatchList. - Future processRedirection( - RouteMatchList routeMatchList, BuildContext context, - {RouteInformation? topRouteInformation}) { - final RouteInformation routeInformation = topRouteInformation ?? - RouteInformation( - location: routeMatchList.uri.toString(), - state: routeMatchList.extra); Future processRedirectorResult(RouteMatchList matches) { if (matches.isEmpty) { return SynchronousFuture(errorScreen( @@ -120,7 +103,7 @@ class GoRouteInformationParser extends RouteInformationParser { final FutureOr redirectorResult = redirector( context, - SynchronousFuture(routeMatchList), + SynchronousFuture(initialMatches), configuration, matcher, extra: routeInformation.state, @@ -145,8 +128,6 @@ class GoRouteInformationParser extends RouteInformationParser { if (configuration.isEmpty) { return null; } - final Object? encodedMatchList = - _matchListCodec.encodeMatchList(configuration); if (configuration.matches.last is ImperativeRouteMatch) { configuration = (configuration.matches.last as ImperativeRouteMatch).matches; @@ -156,7 +137,7 @@ class GoRouteInformationParser extends RouteInformationParser { // https://github.com/flutter/flutter/issues/124045. // ignore: deprecated_member_use location: configuration.uri.toString(), - state: encodedMatchList, + state: configuration.extra, ); } } diff --git a/packages/go_router/lib/src/route.dart b/packages/go_router/lib/src/route.dart index 7e32679bc2f6..b9e0f35117e3 100644 --- a/packages/go_router/lib/src/route.dart +++ b/packages/go_router/lib/src/route.dart @@ -716,6 +716,29 @@ class StatefulShellRoute extends ShellRouteBase { assert(_debugValidateRestorationScopeIds(restorationScopeId, branches)), super._(routes: _routes(branches)); + /// Constructs a StatefulShellRoute that uses an [IndexedStack] for its + /// nested [Navigator]s. + /// + /// This constructor provides an IndexedStack based implementation for the + /// container ([navigatorContainerBuilder]) used to manage the Widgets + /// representing the branch Navigators. A part from that, this constructor + /// works the same way as the default constructor. + /// + /// 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({ + required List branches, + StatefulShellRouteBuilder? builder, + StatefulShellRoutePageBuilder? pageBuilder, + String? restorationScopeId, + }) : this( + branches: branches, + builder: builder, + pageBuilder: pageBuilder, + restorationScopeId: restorationScopeId, + navigatorContainerBuilder: _indexedStackContainerBuilder, + ); + /// Restoration ID to save and restore the state of the navigator, including /// its history. final String? restorationScopeId; @@ -810,6 +833,12 @@ class StatefulShellRoute extends ShellRouteBase { router: GoRouter.of(context), containerBuilder: navigatorContainerBuilder); + static Widget _indexedStackContainerBuilder(BuildContext context, + StatefulNavigationShell navigationShell, List children) { + return _IndexedStackedRouteBranchContainer( + currentIndex: navigationShell.currentIndex, children: children); + } + static List _routes(List branches) => branches.expand((StatefulShellBranch e) => e.routes).toList(); @@ -847,34 +876,6 @@ class StatefulShellRoute extends ShellRouteBase { } } -/// A stateful shell route implementation that uses an [IndexedStack] for its -/// nested [Navigator]s. -/// -/// StackedShellRoute provides an IndexedStack based implementation for the -/// container ([navigatorContainerBuilder]) used to managing the Widgets -/// representing the branch Navigators. StackedShellRoute is created in the same -/// way as [StatefulShellRoute], but without the need to provide a -/// navigatorContainerBuilder parameter. -/// -/// 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 and StackedShellRoute. -class StackedShellRoute extends StatefulShellRoute { - /// Constructs a [StackedShellRoute] from a list of [StatefulShellBranch]es, - /// each representing a separate nested navigation tree (branch). - StackedShellRoute({ - required super.branches, - super.builder, - super.pageBuilder, - super.restorationScopeId, - }) : super(navigatorContainerBuilder: _navigatorContainerBuilder); - - static Widget _navigatorContainerBuilder(BuildContext context, - StatefulNavigationShell navigationShell, List children) { - return _IndexedStackedRouteBranchContainer( - currentIndex: navigationShell.currentIndex, children: children); - } -} - /// Representation of a separate branch in a stateful navigation tree, used to /// configure [StatefulShellRoute]. /// @@ -1169,6 +1170,9 @@ class StatefulNavigationShellState extends State if (matchlist != null && matchlist.isNotEmpty) { final RouteInformation preParsed = matchlist.toPreParsedRouteInformation(); + // TODO(tolo): remove this ignore and migrate the code + // https://github.com/flutter/flutter/issues/124045. + // ignore: deprecated_member_use, unnecessary_non_null_assertion _router.go(preParsed.location!, extra: preParsed.state); } else { _router.go(widget.effectiveInitialBranchLocation(index)); diff --git a/packages/go_router/test/builder_test.dart b/packages/go_router/test/builder_test.dart index 1a29966d62d6..ac97cab5dd75 100644 --- a/packages/go_router/test/builder_test.dart +++ b/packages/go_router/test/builder_test.dart @@ -336,7 +336,7 @@ void main() { initialLocation: '/a', navigatorKey: rootNavigatorKey, routes: [ - StackedShellRoute( + StatefulShellRoute.indexedStack( restorationScopeId: 'shell', builder: (BuildContext context, GoRouterState state, StatefulNavigationShell navigationShell) => diff --git a/packages/go_router/test/configuration_test.dart b/packages/go_router/test/configuration_test.dart index 20a4ba36544b..c76e9d9c174b 100644 --- a/packages/go_router/test/configuration_test.dart +++ b/packages/go_router/test/configuration_test.dart @@ -97,7 +97,7 @@ void main() { RouteConfiguration( navigatorKey: root, routes: [ - StackedShellRoute(branches: [ + StatefulShellRoute.indexedStack(branches: [ StatefulShellBranch( navigatorKey: keyA, routes: [ @@ -135,7 +135,7 @@ void main() { RouteConfiguration( navigatorKey: root, routes: [ - StackedShellRoute(branches: [ + StatefulShellRoute.indexedStack(branches: [ StatefulShellBranch( navigatorKey: keyA, routes: [ @@ -171,7 +171,7 @@ void main() { RouteConfiguration( navigatorKey: root, routes: [ - StackedShellRoute(branches: [ + StatefulShellRoute.indexedStack(branches: [ StatefulShellBranch( routes: [ GoRoute( @@ -221,7 +221,7 @@ void main() { RouteConfiguration( navigatorKey: root, routes: [ - StackedShellRoute(branches: [ + StatefulShellRoute.indexedStack(branches: [ StatefulShellBranch(routes: shellRouteChildren) ], builder: mockStackedShellBuilder), ], @@ -257,7 +257,7 @@ void main() { RouteConfiguration( navigatorKey: root, routes: [ - StackedShellRoute(branches: [ + StatefulShellRoute.indexedStack(branches: [ StatefulShellBranch( routes: [routeA], navigatorKey: sectionANavigatorKey), @@ -290,7 +290,7 @@ void main() { RouteConfiguration( navigatorKey: root, routes: [ - StackedShellRoute(branches: [ + StatefulShellRoute.indexedStack(branches: [ StatefulShellBranch( initialLocation: '/x', navigatorKey: sectionANavigatorKey, @@ -336,7 +336,7 @@ void main() { RouteConfiguration( navigatorKey: root, routes: [ - StackedShellRoute(branches: [ + StatefulShellRoute.indexedStack(branches: [ StatefulShellBranch( initialLocation: '/b', navigatorKey: sectionANavigatorKey, @@ -351,16 +351,18 @@ void main() { initialLocation: '/b', navigatorKey: sectionBNavigatorKey, routes: [ - StackedShellRoute(branches: [ - StatefulShellBranch( - routes: [ - GoRoute( - path: '/b', - builder: _mockScreenBuilder, + StatefulShellRoute.indexedStack( + branches: [ + StatefulShellBranch( + routes: [ + GoRoute( + path: '/b', + builder: _mockScreenBuilder, + ), + ], ), ], - ), - ], builder: mockStackedShellBuilder), + builder: mockStackedShellBuilder), ], ), ], builder: mockStackedShellBuilder), @@ -384,7 +386,7 @@ void main() { RouteConfiguration( navigatorKey: root, routes: [ - StackedShellRoute(branches: [ + StatefulShellRoute.indexedStack(branches: [ StatefulShellBranch( routes: [ GoRoute( @@ -415,7 +417,7 @@ void main() { StatefulShellBranch( initialLocation: '/c/detail', routes: [ - StackedShellRoute(branches: [ + StatefulShellRoute.indexedStack(branches: [ StatefulShellBranch( routes: [ GoRoute( @@ -481,7 +483,7 @@ void main() { final RouteConfiguration config = RouteConfiguration( navigatorKey: GlobalKey(debugLabel: 'root'), routes: [ - StackedShellRoute( + StatefulShellRoute.indexedStack( builder: mockStackedShellBuilder, branches: [ branchA = StatefulShellBranch(routes: [ @@ -493,7 +495,7 @@ void main() { path: 'x', builder: _mockScreenBuilder, routes: [ - StackedShellRoute( + StatefulShellRoute.indexedStack( builder: mockStackedShellBuilder, branches: [ branchY = diff --git a/packages/go_router/test/delegate_test.dart b/packages/go_router/test/delegate_test.dart index 98e82013ac0b..c25877bf1453 100644 --- a/packages/go_router/test/delegate_test.dart +++ b/packages/go_router/test/delegate_test.dart @@ -39,7 +39,7 @@ Future createGoRouterWithStatefulShellRoute( routes: [ GoRoute(path: '/', builder: (_, __) => const DummyStatefulWidget()), GoRoute(path: '/a', builder: (_, __) => const DummyStatefulWidget()), - StackedShellRoute(branches: [ + StatefulShellRoute.indexedStack(branches: [ StatefulShellBranch(routes: [ GoRoute( path: '/c', diff --git a/packages/go_router/test/go_router_test.dart b/packages/go_router/test/go_router_test.dart index 7f9b8f55d98c..29ccb041a421 100644 --- a/packages/go_router/test/go_router_test.dart +++ b/packages/go_router/test/go_router_test.dart @@ -2508,7 +2508,7 @@ void main() { (WidgetTester tester) async { StatefulNavigationShell? routeState; final List routes = [ - StackedShellRoute( + StatefulShellRoute.indexedStack( builder: (BuildContext context, GoRouterState state, StatefulNavigationShell navigationShell) { routeState = navigationShell; @@ -3080,7 +3080,7 @@ void main() { GlobalKey(); final List routes = [ - StackedShellRoute( + StatefulShellRoute.indexedStack( builder: (BuildContext context, GoRouterState state, StatefulNavigationShell navigationShell) => navigationShell, @@ -3125,7 +3125,7 @@ void main() { builder: (BuildContext context, GoRouterState state) => const Text('Root'), routes: [ - StackedShellRoute( + StatefulShellRoute.indexedStack( builder: (BuildContext context, GoRouterState state, StatefulNavigationShell navigationShell) => navigationShell, @@ -3171,7 +3171,7 @@ void main() { StatefulNavigationShell? routeState; final List routes = [ - StackedShellRoute( + StatefulShellRoute.indexedStack( builder: (BuildContext context, GoRouterState state, StatefulNavigationShell navigationShell) { routeState = navigationShell; @@ -3263,7 +3263,7 @@ void main() { StatefulNavigationShell? routeState; final List routes = [ - StackedShellRoute( + StatefulShellRoute.indexedStack( builder: (BuildContext context, GoRouterState state, StatefulNavigationShell navigationShell) { routeState = navigationShell; @@ -3334,7 +3334,7 @@ void main() { StatefulNavigationShell? routeState2; final List routes = [ - StackedShellRoute( + StatefulShellRoute.indexedStack( builder: (BuildContext context, GoRouterState state, StatefulNavigationShell navigationShell) { routeState1 = navigationShell; @@ -3342,7 +3342,7 @@ void main() { }, branches: [ StatefulShellBranch(routes: [ - StackedShellRoute( + StatefulShellRoute.indexedStack( builder: (BuildContext context, GoRouterState state, StatefulNavigationShell navigationShell) { routeState2 = navigationShell; @@ -3432,7 +3432,7 @@ void main() { StatefulNavigationShell? routeState; final List routes = [ - StackedShellRoute( + StatefulShellRoute.indexedStack( builder: (BuildContext context, GoRouterState state, StatefulNavigationShell navigationShell) { routeState = navigationShell; @@ -3517,7 +3517,7 @@ void main() { StatefulNavigationShell? routeState; final List routes = [ - StackedShellRoute( + StatefulShellRoute.indexedStack( builder: (BuildContext context, GoRouterState state, StatefulNavigationShell navigationShell) { routeState = navigationShell; @@ -3576,7 +3576,7 @@ void main() { builder: (BuildContext context, GoRouterState state) => Text('Common - ${state.extra}'), ), - StackedShellRoute( + StatefulShellRoute.indexedStack( builder: (BuildContext context, GoRouterState state, StatefulNavigationShell navigationShell) { routeState = navigationShell; @@ -3635,7 +3635,7 @@ void main() { StatefulNavigationShell? routeState; final List routes = [ - StackedShellRoute( + StatefulShellRoute.indexedStack( builder: (BuildContext context, GoRouterState state, StatefulNavigationShell navigationShell) { routeState = navigationShell; @@ -3734,7 +3734,7 @@ void main() { final List routes = [ // First level shell - StackedShellRoute( + StatefulShellRoute.indexedStack( builder: (BuildContext context, GoRouterState state, StatefulNavigationShell navigationShell) { routeState = navigationShell; @@ -3750,7 +3750,7 @@ void main() { ]), StatefulShellBranch(routes: [ // Second level / nested shell - StackedShellRoute( + StatefulShellRoute.indexedStack( builder: (BuildContext context, GoRouterState state, StatefulNavigationShell navigationShell) => navigationShell, @@ -3962,7 +3962,7 @@ void main() { navigatorKey: rootNavigatorKey, initialLocation: '/a', routes: [ - StackedShellRoute( + StatefulShellRoute.indexedStack( builder: mockStackedShellBuilder, branches: [ StatefulShellBranch(routes: [ @@ -4383,7 +4383,7 @@ void main() { StatefulNavigationShell? routeState; final List routes = [ - StackedShellRoute( + StatefulShellRoute.indexedStack( restorationScopeId: 'shell', pageBuilder: (BuildContext context, GoRouterState state, StatefulNavigationShell navigationShell) { @@ -4515,7 +4515,7 @@ void main() { StatefulNavigationShell? routeStateNested; final List routes = [ - StackedShellRoute( + StatefulShellRoute.indexedStack( restorationScopeId: 'shell', pageBuilder: (BuildContext context, GoRouterState state, StatefulNavigationShell navigationShell) { @@ -4550,7 +4550,7 @@ void main() { StatefulShellBranch( restorationScopeId: 'branchB', routes: [ - StackedShellRoute( + StatefulShellRoute.indexedStack( restorationScopeId: 'branchB-nested-shell', pageBuilder: (BuildContext context, GoRouterState state, StatefulNavigationShell navigationShell) { From 9eaf1af15ebf9ccefcfc1e6d343c3ce03fa5c918 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20Lo=CC=88fstrand?= Date: Thu, 11 May 2023 00:17:27 +0200 Subject: [PATCH 107/112] Some additional cleanup --- packages/go_router/lib/src/builder.dart | 4 +-- packages/go_router/lib/src/route.dart | 4 +-- .../go_router/test/configuration_test.dart | 16 +++++----- packages/go_router/test/go_router_test.dart | 30 +++++++++---------- 4 files changed, 27 insertions(+), 27 deletions(-) diff --git a/packages/go_router/lib/src/builder.dart b/packages/go_router/lib/src/builder.dart index 37b2ede44eb9..f1512517074b 100644 --- a/packages/go_router/lib/src/builder.dart +++ b/packages/go_router/lib/src/builder.dart @@ -60,6 +60,8 @@ class RouteBuilder { /// changes. final List observers; + final GoRouterStateRegistry _registry = GoRouterStateRegistry(); + /// A callback called when a `route` produced by `match` is about to be popped /// with the `result`. /// @@ -68,8 +70,6 @@ class RouteBuilder { /// If this method returns false, this builder aborts the pop. final PopPageWithRouteMatchCallback onPopPageWithRouteMatch; - final GoRouterStateRegistry _registry = GoRouterStateRegistry(); - /// Caches a HeroController for the nested Navigator, which solves cases where the /// Hero Widget animation stops working when navigating. // TODO(chunhtai): Remove _goHeroCache once below issue is fixed: diff --git a/packages/go_router/lib/src/route.dart b/packages/go_router/lib/src/route.dart index b9e0f35117e3..d19bf40842ee 100644 --- a/packages/go_router/lib/src/route.dart +++ b/packages/go_router/lib/src/route.dart @@ -619,7 +619,7 @@ class ShellRoute extends ShellRouteBase { /// when switching active branch. /// /// For a default implementation of [navigatorContainerBuilder], consider using -/// [StackedShellRoute]. +/// [StatefulShellRoute]. /// /// Below is a simple example of how a router configuration with /// StatefulShellRoute could be achieved. In this example, a @@ -692,7 +692,7 @@ class ShellRoute 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 and StackedShellRoute. +/// for a complete runnable example using StatefulShellRoute and StatefulShellRoute. class StatefulShellRoute extends ShellRouteBase { /// Constructs a [StatefulShellRoute] from a list of [StatefulShellBranch]es, /// each representing a separate nested navigation tree (branch). diff --git a/packages/go_router/test/configuration_test.dart b/packages/go_router/test/configuration_test.dart index c76e9d9c174b..312470221842 100644 --- a/packages/go_router/test/configuration_test.dart +++ b/packages/go_router/test/configuration_test.dart @@ -83,7 +83,7 @@ void main() { }); test( - 'throws when StackedShellRoute sub-route uses incorrect parentNavigatorKey', + 'throws when StatefulShellRoute sub-route uses incorrect parentNavigatorKey', () { final GlobalKey root = GlobalKey(debugLabel: 'root'); @@ -125,7 +125,7 @@ void main() { }); test( - 'does not throw when StackedShellRoute sub-route uses correct parentNavigatorKeys', + 'does not throw when StatefulShellRoute sub-route uses correct parentNavigatorKeys', () { final GlobalKey root = GlobalKey(debugLabel: 'root'); @@ -160,7 +160,7 @@ void main() { }); test( - 'throws when a sub-route of StackedShellRoute has a parentNavigatorKey', + 'throws when a sub-route of StatefulShellRoute has a parentNavigatorKey', () { final GlobalKey root = GlobalKey(debugLabel: 'root'); @@ -205,7 +205,7 @@ void main() { ); }); - test('throws when StackedShellRoute has duplicate navigator keys', () { + test('throws when StatefulShellRoute has duplicate navigator keys', () { final GlobalKey root = GlobalKey(debugLabel: 'root'); final GlobalKey keyA = @@ -236,7 +236,7 @@ void main() { }); test( - 'throws when a child of StackedShellRoute has an incorrect ' + 'throws when a child of StatefulShellRoute has an incorrect ' 'parentNavigatorKey', () { final GlobalKey root = GlobalKey(debugLabel: 'root'); @@ -277,7 +277,7 @@ void main() { }); test( - 'throws when a branch of a StackedShellRoute has an incorrect ' + 'throws when a branch of a StatefulShellRoute has an incorrect ' 'initialLocation', () { final GlobalKey root = GlobalKey(debugLabel: 'root'); @@ -323,7 +323,7 @@ void main() { }); test( - 'throws when a branch of a StackedShellRoute has a initialLocation ' + 'throws when a branch of a StatefulShellRoute has a initialLocation ' 'that is not a descendant of the same branch', () { final GlobalKey root = GlobalKey(debugLabel: 'root'); @@ -378,7 +378,7 @@ void main() { }); test( - 'does not throw when a branch of a StackedShellRoute has correctly ' + 'does not throw when a branch of a StatefulShellRoute has correctly ' 'configured initialLocations', () { final GlobalKey root = GlobalKey(debugLabel: 'root'); diff --git a/packages/go_router/test/go_router_test.dart b/packages/go_router/test/go_router_test.dart index 29ccb041a421..532a4f64f015 100644 --- a/packages/go_router/test/go_router_test.dart +++ b/packages/go_router/test/go_router_test.dart @@ -2504,7 +2504,7 @@ void main() { expect(imperativeRouteMatch.matches.pathParameters['pid'], pid); }); - testWidgets('StackedShellRoute supports nested routes with params', + testWidgets('StatefulShellRoute supports nested routes with params', (WidgetTester tester) async { StatefulNavigationShell? routeState; final List routes = [ @@ -3075,7 +3075,7 @@ void main() { expect(find.text('Screen C'), findsNothing); }); - testWidgets('Builds StackedShellRoute', (WidgetTester tester) async { + testWidgets('Builds StatefulShellRoute', (WidgetTester tester) async { final GlobalKey rootNavigatorKey = GlobalKey(); @@ -3114,7 +3114,7 @@ void main() { expect(find.text('Screen B'), findsOneWidget); }); - testWidgets('Builds StackedShellRoute as a sub-route', + testWidgets('Builds StatefulShellRoute as a sub-route', (WidgetTester tester) async { final GlobalKey rootNavigatorKey = GlobalKey(); @@ -3162,7 +3162,7 @@ void main() { }); testWidgets( - 'Navigation with goBranch is correctly handled in StackedShellRoute', + 'Navigation with goBranch is correctly handled in StatefulShellRoute', (WidgetTester tester) async { final GlobalKey rootNavigatorKey = GlobalKey(); @@ -3254,7 +3254,7 @@ void main() { }); testWidgets( - 'Navigates to correct nested navigation tree in StackedShellRoute ' + 'Navigates to correct nested navigation tree in StatefulShellRoute ' 'and maintains state', (WidgetTester tester) async { final GlobalKey rootNavigatorKey = GlobalKey(); @@ -3324,7 +3324,7 @@ void main() { expect(statefulWidgetKey.currentState?.counter, equals(0)); }); - testWidgets('Maintains state for nested StackedShellRoute', + testWidgets('Maintains state for nested StatefulShellRoute', (WidgetTester tester) async { final GlobalKey rootNavigatorKey = GlobalKey(); @@ -3421,7 +3421,7 @@ void main() { }); testWidgets( - 'Pops from the correct Navigator in a StackedShellRoute when the ' + 'Pops from the correct Navigator in a StatefulShellRoute when the ' 'Android back button is pressed', (WidgetTester tester) async { final GlobalKey rootNavigatorKey = GlobalKey(); @@ -3511,7 +3511,7 @@ void main() { testWidgets( 'Maintains extra navigation information when navigating ' - 'between branches in StackedShellRoute', (WidgetTester tester) async { + 'between branches in StatefulShellRoute', (WidgetTester tester) async { final GlobalKey rootNavigatorKey = GlobalKey(); StatefulNavigationShell? routeState; @@ -3564,7 +3564,7 @@ void main() { testWidgets( 'Pushed non-descendant routes are correctly restored when ' - 'navigating between branches in StackedShellRoute', + 'navigating between branches in StatefulShellRoute', (WidgetTester tester) async { final GlobalKey rootNavigatorKey = GlobalKey(); @@ -3629,7 +3629,7 @@ void main() { testWidgets( 'Redirects are correctly handled when switching branch in a ' - 'StackedShellRoute', (WidgetTester tester) async { + 'StatefulShellRoute', (WidgetTester tester) async { final GlobalKey rootNavigatorKey = GlobalKey(); StatefulNavigationShell? routeState; @@ -3724,7 +3724,7 @@ void main() { }); testWidgets( - 'Pushed top-level route is correctly handled by StackedShellRoute', + 'Pushed top-level route is correctly handled by StatefulShellRoute', (WidgetTester tester) async { final GlobalKey rootNavigatorKey = GlobalKey(); @@ -3954,7 +3954,7 @@ void main() { ); testWidgets( - 'It checks if StackedShellRoute navigators can pop', + 'It checks if StatefulShellRoute navigators can pop', (WidgetTester tester) async { final GlobalKey rootNavigatorKey = GlobalKey(); @@ -4015,7 +4015,7 @@ void main() { expect(find.text('Screen B detail', skipOffstage: false), findsOneWidget); expect(router.canPop(), true); - // Verify that it is actually the StackedShellRoute that reports + // Verify that it is actually the StatefulShellRoute that reports // canPop = true expect(rootNavigatorKey.currentState?.canPop(), false); }, @@ -4370,7 +4370,7 @@ void main() { expect(statefulWidgetKeyA.currentState?.counter, equals(1)); }); - testWidgets('Restores state of branches in StackedShellRoute correctly', + testWidgets('Restores state of branches in StatefulShellRoute correctly', (WidgetTester tester) async { final GlobalKey rootNavigatorKey = GlobalKey(); @@ -4503,7 +4503,7 @@ void main() { }); testWidgets( - 'Restores state of imperative routes in StackedShellRoute correctly', + 'Restores state of imperative routes in StatefulShellRoute correctly', (WidgetTester tester) async { final GlobalKey rootNavigatorKey = GlobalKey(); From 57b761f6253ea36bda75fb986194115762bdcdf6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20L=C3=B6fstrand?= Date: Thu, 18 May 2023 17:20:24 +0200 Subject: [PATCH 108/112] Apply suggestions from code review Co-authored-by: John Ryan Co-authored-by: chunhtai <47866232+chunhtai@users.noreply.github.com> --- packages/go_router/example/lib/stateful_shell_route.dart | 6 +++--- packages/go_router/lib/src/route.dart | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/go_router/example/lib/stateful_shell_route.dart b/packages/go_router/example/lib/stateful_shell_route.dart index 7e9b0b92eeb7..cd1f59ea7fa1 100644 --- a/packages/go_router/example/lib/stateful_shell_route.dart +++ b/packages/go_router/example/lib/stateful_shell_route.dart @@ -16,9 +16,9 @@ final GlobalKey _sectionANavigatorKey = // enables deep linking into nested pages. // // This example demonstrates how to display routes within a StatefulShellRoute, -// that are places on separate navigators. The example also demonstrates how -// state is maintained when switching between different bar items (and thus -// branches and Navigators). +// that are placed on separate navigators. The example also demonstrates how +// state is maintained when switching between different bar items (which switches +// between branches and Navigators). void main() { runApp(NestedTabNavigationExampleApp()); diff --git a/packages/go_router/lib/src/route.dart b/packages/go_router/lib/src/route.dart index d19bf40842ee..609a780ada7d 100644 --- a/packages/go_router/lib/src/route.dart +++ b/packages/go_router/lib/src/route.dart @@ -578,8 +578,8 @@ class ShellRoute extends ShellRouteBase { /// sub-routes. /// /// Similar to [ShellRoute], this route class places its sub-route on a -/// different Navigator than the root Navigator. However, this route class -/// differs in that it creates separate Navigators for each of its nested +/// different Navigator than the root [Navigator]. However, this route class +/// differs in that it creates separate [Navigator]s for each of its nested /// branches (i.e. parallel navigation trees), making it possible to build an /// app with stateful nested navigation. This is convenient when for instance /// implementing a UI with a [BottomNavigationBar], with a persistent navigation From 85fa0b6e05784f1ce488a30816521b8dd6c4159f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20Lo=CC=88fstrand?= Date: Thu, 18 May 2023 19:30:33 +0200 Subject: [PATCH 109/112] Apply suggestions from code review. Minor additional cleanup. --- packages/go_router/CHANGELOG.md | 5 +- packages/go_router/example/README.md | 2 +- .../others/custom_stateful_shell_route.dart | 118 +++++++++++++----- .../stateful_shell_state_restoration.dart | 12 +- .../example/lib/stateful_shell_route.dart | 29 ++--- packages/go_router/lib/src/route.dart | 93 +++----------- 6 files changed, 124 insertions(+), 135 deletions(-) diff --git a/packages/go_router/CHANGELOG.md b/packages/go_router/CHANGELOG.md index 993da247fb8c..5e734e647d1c 100644 --- a/packages/go_router/CHANGELOG.md +++ b/packages/go_router/CHANGELOG.md @@ -1,12 +1,9 @@ ## 7.1.0 -- Introduces `StatefulShellRoute` to support using separate - navigators for child routes as well as preserving state in each navigation tree - (flutter/flutter#99124). +- Introduces `StatefulShellRoute` to support using separate navigators for child routes as well as preserving state in each navigation tree (flutter/flutter#99124). - Updates documentation for `pageBuilder` and `builder` fields of `ShellRoute`, to more correctly describe the meaning of the child argument in the builder functions. - Adds support for restorationId to ShellRoute (and StatefulShellRoute). -- Adds support for restoring imperatively pushed routes. ## 7.0.1 diff --git a/packages/go_router/example/README.md b/packages/go_router/example/README.md index 5f2a9957bc75..f4c1c0f48d35 100644 --- a/packages/go_router/example/README.md +++ b/packages/go_router/example/README.md @@ -30,7 +30,7 @@ An example to demonstrate how to use redirect to handle a synchronous sign-in fl An example to demonstrate how to use handle a sign-in flow with a stream authentication service. -## [Stateful Nested Navigation](https://github.com/flutter/packages/blob/main/packages/go_router/example/lib/stacked_shell_route.dart) +## [Stateful Nested Navigation](https://github.com/flutter/packages/blob/main/packages/go_router/example/lib/stateful_shell_route.dart) `flutter run lib/stacked_shell_route.dart` An example to demonstrate how to use a `StatefulShellRoute` to create stateful nested navigation, with a 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 f9448c3bf110..2c0a66fd9e82 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 @@ -32,7 +32,27 @@ class NestedTabNavigationExampleApp extends StatelessWidget { navigatorKey: _rootNavigatorKey, initialLocation: '/a', routes: [ - StatefulShellRoute.indexedStack( + 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) { + // 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); + }, branches: [ // The route branch for the first tab of the bottom navigation bar. StatefulShellBranch( @@ -67,6 +87,23 @@ 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; + }, + 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: [ @@ -109,37 +146,10 @@ class NestedTabNavigationExampleApp extends StatelessWidget { ), ]), ], - builder: (BuildContext context, GoRouterState state, - StatefulNavigationShell navigationShell) { - // This nested StatefulShellRoute demonstrates the use of a - // custom container (TabBarView) 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) => - // Returning a customized container for the branch - // Navigators (i.e. the `List children` argument). - // - // See TabbedRootScreen for more details on how the children - // are used in the TabBarView. - TabbedRootScreen( - navigationShell: navigationShell, children: children), ), ], ), ], - builder: (BuildContext context, GoRouterState state, - StatefulNavigationShell navigationShell) { - // The builder uses the standard way of building the custom shell for - // a IndexedStack based StatefulShellRoute. - return ScaffoldWithNavBar(navigationShell: navigationShell); - }, ), ], ); @@ -162,16 +172,24 @@ class ScaffoldWithNavBar extends StatelessWidget { /// Constructs an [ScaffoldWithNavBar]. const ScaffoldWithNavBar({ required this.navigationShell, + required this.children, Key? key, }) : super(key: key ?? const ValueKey('ScaffoldWithNavBar')); /// The navigation shell and container for the branch Navigators. final StatefulNavigationShell navigationShell; + /// The children (branch Navigators) to display in a custom container + /// ([AnimatedBranchContainer]). + final List children; + @override Widget build(BuildContext context) { return Scaffold( - body: navigationShell, + body: AnimatedBranchContainer( + currentIndex: navigationShell.currentIndex, + children: children, + ), bottomNavigationBar: BottomNavigationBar( // Here, the items of BottomNavigationBar are hard coded. In a real // world scenario, the items would most likely be generated from the @@ -206,6 +224,46 @@ class ScaffoldWithNavBar extends StatelessWidget { } } +/// Custom branch Navigator container that provides animated transitions +/// when switching branches. +class AnimatedBranchContainer extends StatelessWidget { + /// Creates a AnimatedBranchContainer + const AnimatedBranchContainer( + {super.key, required this.currentIndex, required this.children}); + + /// The index (in [children]) of the branch Navigator to display. + final int currentIndex; + + /// The children (branch Navigators) to display in this container. + final List children; + + @override + Widget build(BuildContext context) { + return Stack( + children: children.mapIndexed( + (int index, Widget navigator) { + return AnimatedScale( + scale: index == currentIndex ? 1 : 1.5, + duration: const Duration(milliseconds: 400), + child: AnimatedOpacity( + opacity: index == currentIndex ? 1 : 0, + duration: const Duration(milliseconds: 400), + child: _branchNavigatorWrapper(index, navigator), + ), + ); + }, + ).toList()); + } + + Widget _branchNavigatorWrapper(int index, Widget navigator) => IgnorePointer( + ignoring: index != currentIndex, + child: TickerMode( + enabled: index == currentIndex, + child: navigator, + ), + ); +} + /// Widget for the root page for the first section of the bottom navigation bar. class RootScreenA extends StatelessWidget { /// Creates a RootScreenA @@ -326,7 +384,7 @@ class TabbedRootScreen extends StatefulWidget { /// The current state of the parent StatefulShellRoute. final StatefulNavigationShell navigationShell; - /// The children (Navigators) to display in the [TabBarView]. + /// The children (branch Navigators) to display in the [TabBarView]. final List children; @override 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 d255582fbf5b..aeecd115590f 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,6 +19,12 @@ class RestorableStatefulShellRouteExampleApp extends StatelessWidget { routes: [ StatefulShellRoute.indexedStack( restorationScopeId: 'shell1', + pageBuilder: (BuildContext context, GoRouterState state, + StatefulNavigationShell navigationShell) { + return MaterialPage( + restorationId: 'shellWidget1', + child: ScaffoldWithNavBar(navigationShell: navigationShell)); + }, branches: [ // The route branch for the first tab of the bottom navigation bar. StatefulShellBranch( @@ -77,12 +83,6 @@ class RestorableStatefulShellRouteExampleApp extends StatelessWidget { ], ), ], - pageBuilder: (BuildContext context, GoRouterState state, - StatefulNavigationShell navigationShell) { - return MaterialPage( - restorationId: 'shellWidget1', - child: ScaffoldWithNavBar(navigationShell: navigationShell)); - }, ), ], ); diff --git a/packages/go_router/example/lib/stateful_shell_route.dart b/packages/go_router/example/lib/stateful_shell_route.dart index cd1f59ea7fa1..3732d4c71248 100644 --- a/packages/go_router/example/lib/stateful_shell_route.dart +++ b/packages/go_router/example/lib/stateful_shell_route.dart @@ -14,11 +14,6 @@ final GlobalKey _sectionANavigatorKey = // BottomNavigationBar, where each bar item uses its own persistent navigator, // i.e. navigation state is maintained separately for each item. This setup also // enables deep linking into nested pages. -// -// This example demonstrates how to display routes within a StatefulShellRoute, -// that are placed on separate navigators. The example also demonstrates how -// state is maintained when switching between different bar items (which switches -// between branches and Navigators). void main() { runApp(NestedTabNavigationExampleApp()); @@ -34,6 +29,14 @@ class NestedTabNavigationExampleApp extends StatelessWidget { initialLocation: '/a', routes: [ StatefulShellRoute.indexedStack( + builder: (BuildContext context, GoRouterState state, + StatefulNavigationShell navigationShell) { + // 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); + }, branches: [ // The route branch for the first tab of the bottom navigation bar. StatefulShellBranch( @@ -114,22 +117,6 @@ class NestedTabNavigationExampleApp extends StatelessWidget { ], ), ], - builder: (BuildContext context, GoRouterState state, - StatefulNavigationShell navigationShell) { - // 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); - }, - - // If it's necessary to customize the Page for StatefulShellRoute, - // provide a pageBuilder function instead of the builder, for example: - // pageBuilder: (BuildContext context, GoRouterState state, - // StatefulNavigationShell navigationShell) { - // return NoTransitionPage( - // child: ScaffoldWithNavBar(navigationShell: navigationShell)); - // }, ), ], ); diff --git a/packages/go_router/lib/src/route.dart b/packages/go_router/lib/src/route.dart index 609a780ada7d..6e74ace25aa2 100644 --- a/packages/go_router/lib/src/route.dart +++ b/packages/go_router/lib/src/route.dart @@ -618,81 +618,28 @@ class ShellRoute extends ShellRouteBase { /// [Offstage] handling etc) of the branch Navigators and any animations needed /// when switching active branch. /// -/// For a default implementation of [navigatorContainerBuilder], consider using -/// [StatefulShellRoute]. -/// -/// Below is a simple example of how a router configuration with -/// StatefulShellRoute could be achieved. In this example, a -/// BottomNavigationBar with two tabs is used, and each of the tabs gets its -/// own Navigator. A container widget responsible for managing the Navigators -/// for all route branches will then be passed as the child argument -/// of the builder function. +/// For a default implementation of [navigatorContainerBuilder] that is +/// appropriate for most use cases, consider using the constructor +/// [StatefulShellRoute.indexedStack]. /// -/// ``` -/// final GlobalKey _tabANavigatorKey = -/// GlobalKey(debugLabel: 'tabANavigator'); -/// final GlobalKey _tabBNavigatorKey = -/// GlobalKey(debugLabel: 'tabBNavigator'); -/// -/// final GoRouter _router = GoRouter( -/// initialLocation: '/a', -/// routes: [ -/// StatefulShellRoute( -/// builder: (BuildContext context, GoRouterState state, -/// StatefulNavigationShell navigationShell) { -/// return ScaffoldWithNavBar(navigationShell: navigationShell); -/// }, -/// navigatorContainerBuilder: (BuildContext context, -/// StatefulNavigationShell navigationShell, -/// List children) => -/// MyCustomContainer(children: children), -/// branches: [ -/// /// The first branch, i.e. tab 'A' -/// StatefulShellBranch( -/// navigatorKey: _tabANavigatorKey, -/// routes: [ -/// GoRoute( -/// path: '/a', -/// builder: (BuildContext context, GoRouterState state) => -/// const RootScreen(label: 'A', detailsPath: '/a/details'), -/// routes: [ -/// /// Will cover screen A but not the bottom navigation bar -/// GoRoute( -/// path: 'details', -/// builder: (BuildContext context, GoRouterState state) => -/// const DetailsScreen(label: 'A'), -/// ), -/// ], -/// ), -/// ], -/// ), -/// /// The second branch, i.e. tab 'B' -/// StatefulShellBranch( -/// navigatorKey: _tabBNavigatorKey, -/// routes: [ -/// GoRoute( -/// path: '/b', -/// builder: (BuildContext context, GoRouterState state) => -/// const RootScreen(label: 'B', detailsPath: '/b/details'), -/// routes: [ -/// /// Will cover screen B but not the bottom navigation bar -/// GoRoute( -/// path: 'details', -/// builder: (BuildContext context, GoRouterState state) => -/// const DetailsScreen(label: 'B'), -/// ), -/// ], -/// ), -/// ], -/// ), -/// ], -/// ), -/// ], -/// ); -/// ``` +/// With StatefulShellRoute (and any route below it), animated transitions +/// between routes in the same navigation stack works the same way as with other +/// route classes, and can be customized using pageBuilder. However, since +/// StatefulShellRoute maintains a set of parallel navigation stacks, +/// any transitions when switching between branches is the responsibility of the +/// branch Navigator container (i.e. [navigatorContainerBuilder]). The default +/// [IndexedStack] implementation ([StatefulShellRoute.indexedStack]) does not +/// use animated transitions, but an example is provided on how to accomplish +/// this (see link to custom StatefulShellRoute example below). /// -/// 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 and StatefulShellRoute. +/// See also: +/// * [StatefulShellRoute.indexedStack] which provides a default +/// StatefulShellRoute implementation suitable for most use cases. +/// * [Stateful Nested Navigation example](https://github.com/flutter/packages/blob/main/packages/go_router/example/lib/stateful_shell_route.dart) +/// for a complete runnable example using StatefulShellRoute. +/// * [Custom StatefulShellRoute example](https://github.com/flutter/packages/blob/main/packages/go_router/example/lib/others/custom_stateful_shell_route.dart) +/// which demonstrates how to customize the container for the branch Navigators +/// and how to implement animated transitions when switching branches. class StatefulShellRoute extends ShellRouteBase { /// Constructs a [StatefulShellRoute] from a list of [StatefulShellBranch]es, /// each representing a separate nested navigation tree (branch). From d75c5e28d01f4073f87b32d77f16302ea180afde Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20Lo=CC=88fstrand?= Date: Thu, 18 May 2023 20:29:07 +0200 Subject: [PATCH 110/112] Reintroduced resetLocation parameter to goBranch. --- .../others/custom_stateful_shell_route.dart | 24 ++++++++--------- .../example/lib/stateful_shell_route.dart | 24 ++++++++--------- packages/go_router/lib/src/route.dart | 26 ++++++++++--------- 3 files changed, 36 insertions(+), 38 deletions(-) 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 2c0a66fd9e82..9fb73dcf7123 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 @@ -208,19 +208,17 @@ 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) { - if (index != navigationShell.currentIndex) { - // 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); - } else { - // If tapping the bar item for the branch that is already active, go to - // the initial location instead. The method - // effectiveInitialBranchLocation can be used to get the (implicitly or - // explicitly) configured initial location in a convenient way. - GoRouter.of(context) - .go(navigationShell.effectiveInitialBranchLocation(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 resetLocation parameter of goBranch. + resetLocation: index == navigationShell.currentIndex, + ); } } diff --git a/packages/go_router/example/lib/stateful_shell_route.dart b/packages/go_router/example/lib/stateful_shell_route.dart index 3732d4c71248..9e09636869f3 100644 --- a/packages/go_router/example/lib/stateful_shell_route.dart +++ b/packages/go_router/example/lib/stateful_shell_route.dart @@ -168,19 +168,17 @@ 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) { - if (index != navigationShell.currentIndex) { - // 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); - } else { - // If tapping the bar item for the branch that is already active, go to - // the initial location instead. The method - // effectiveInitialBranchLocation can be used to get the (implicitly or - // explicitly) configured initial location in a convenient way. - GoRouter.of(context) - .go(navigationShell.effectiveInitialBranchLocation(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 resetLocation parameter of goBranch. + resetLocation: index == navigationShell.currentIndex, + ); } } diff --git a/packages/go_router/lib/src/route.dart b/packages/go_router/lib/src/route.dart index 6e74ace25aa2..7154a949e630 100644 --- a/packages/go_router/lib/src/route.dart +++ b/packages/go_router/lib/src/route.dart @@ -945,18 +945,18 @@ class StatefulNavigationShell extends StatefulWidget { /// 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, this - /// method will navigate to initial location of the branch (see - /// [StatefulShellBranch.initialLocation]). - void goBranch(int index) { + /// [StatefulShellRoute]. If the branch has not been visited before, or if + /// resetLocation is true, this method will navigate to initial location of + /// the branch (see [StatefulShellBranch.initialLocation]). + void goBranch(int index, {bool resetLocation = false}) { final StatefulShellRoute route = shellRouteContext.route as StatefulShellRoute; final StatefulNavigationShellState? shellState = route._shellStateKey.currentState; if (shellState != null) { - shellState.goBranch(index); + shellState.goBranch(index, resetLocation: resetLocation); } else { - _router.go(effectiveInitialBranchLocation(index)); + _router.go(_effectiveInitialBranchLocation(index)); } } @@ -966,7 +966,7 @@ class StatefulNavigationShell extends StatefulWidget { /// The effective initial location is either the /// [StackedShellBranch.initialLocation], if specified, or the location of the /// [StackedShellBranch.defaultRoute]. - String effectiveInitialBranchLocation(int index) { + String _effectiveInitialBranchLocation(int index) { final StatefulShellRoute route = shellRouteContext.route as StatefulShellRoute; final StatefulShellBranch branch = route.branches[index]; @@ -1109,11 +1109,13 @@ class StatefulNavigationShellState extends State /// 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, this - /// method will navigate to initial location of the branch (see - void goBranch(int index) { + /// [StatefulShellRoute]. If the branch has not been visited before, or if + /// resetLocation is true, this method will navigate to initial location of + /// the branch (see [StatefulShellBranch.initialLocation]). + void goBranch(int index, {bool resetLocation = false}) { assert(index >= 0 && index < route.branches.length); - final RouteMatchList? matchlist = _matchListForBranch(index); + final RouteMatchList? matchlist = + resetLocation ? null : _matchListForBranch(index); if (matchlist != null && matchlist.isNotEmpty) { final RouteInformation preParsed = matchlist.toPreParsedRouteInformation(); @@ -1122,7 +1124,7 @@ class StatefulNavigationShellState extends State // ignore: deprecated_member_use, unnecessary_non_null_assertion _router.go(preParsed.location!, extra: preParsed.state); } else { - _router.go(widget.effectiveInitialBranchLocation(index)); + _router.go(widget._effectiveInitialBranchLocation(index)); } } From c13cd7116b8c751ab8636c2991e326fd441a8ba2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20Lo=CC=88fstrand?= Date: Fri, 19 May 2023 17:12:28 +0200 Subject: [PATCH 111/112] Renamed parameter resetLocation in goBranch to initialLocation. --- .../lib/others/custom_stateful_shell_route.dart | 4 ++-- .../go_router/example/lib/stateful_shell_route.dart | 4 ++-- packages/go_router/lib/src/route.dart | 12 ++++++------ 3 files changed, 10 insertions(+), 10 deletions(-) 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 9fb73dcf7123..5fbe2b6d2868 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 @@ -216,8 +216,8 @@ class ScaffoldWithNavBar extends StatelessWidget { // 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 resetLocation parameter of goBranch. - resetLocation: index == navigationShell.currentIndex, + // using the initialLocation parameter of goBranch. + initialLocation: index == navigationShell.currentIndex, ); } } diff --git a/packages/go_router/example/lib/stateful_shell_route.dart b/packages/go_router/example/lib/stateful_shell_route.dart index 9e09636869f3..eb0a67b679de 100644 --- a/packages/go_router/example/lib/stateful_shell_route.dart +++ b/packages/go_router/example/lib/stateful_shell_route.dart @@ -176,8 +176,8 @@ class ScaffoldWithNavBar extends StatelessWidget { // 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 resetLocation parameter of goBranch. - resetLocation: index == navigationShell.currentIndex, + // using the initialLocation parameter of goBranch. + initialLocation: index == navigationShell.currentIndex, ); } } diff --git a/packages/go_router/lib/src/route.dart b/packages/go_router/lib/src/route.dart index 7154a949e630..69b27e3694ad 100644 --- a/packages/go_router/lib/src/route.dart +++ b/packages/go_router/lib/src/route.dart @@ -946,15 +946,15 @@ class StatefulNavigationShell extends StatefulWidget { /// /// This method will switch the currently active branch [Navigator] for the /// [StatefulShellRoute]. If the branch has not been visited before, or if - /// resetLocation is true, this method will navigate to initial location of + /// initialLocation is true, this method will navigate to initial location of /// the branch (see [StatefulShellBranch.initialLocation]). - void goBranch(int index, {bool resetLocation = false}) { + 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, resetLocation: resetLocation); + shellState.goBranch(index, initialLocation: initialLocation); } else { _router.go(_effectiveInitialBranchLocation(index)); } @@ -1110,12 +1110,12 @@ class StatefulNavigationShellState extends State /// /// This method will switch the currently active branch [Navigator] for the /// [StatefulShellRoute]. If the branch has not been visited before, or if - /// resetLocation is true, this method will navigate to initial location of + /// initialLocation is true, this method will navigate to initial location of /// the branch (see [StatefulShellBranch.initialLocation]). - void goBranch(int index, {bool resetLocation = false}) { + void goBranch(int index, {bool initialLocation = false}) { assert(index >= 0 && index < route.branches.length); final RouteMatchList? matchlist = - resetLocation ? null : _matchListForBranch(index); + initialLocation ? null : _matchListForBranch(index); if (matchlist != null && matchlist.isNotEmpty) { final RouteInformation preParsed = matchlist.toPreParsedRouteInformation(); From c7b1295f6948777b5da398ec8ae99e0f9e138c1a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20Lo=CC=88fstrand?= Date: Fri, 19 May 2023 19:23:33 +0200 Subject: [PATCH 112/112] Fixed future deprecation warning workaround --- packages/go_router/lib/src/matching.dart | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/go_router/lib/src/matching.dart b/packages/go_router/lib/src/matching.dart index e295b526f94d..b809ccae5c06 100644 --- a/packages/go_router/lib/src/matching.dart +++ b/packages/go_router/lib/src/matching.dart @@ -244,10 +244,10 @@ class RouteMatchList { /// Returns a pre-parsed [RouteInformation], containing a reference to this /// match list. RouteInformation toPreParsedRouteInformation() { - // TODO(tolo): remove this ignore and migrate the code - // https://github.com/flutter/flutter/issues/124045. - // ignore: deprecated_member_use return RouteInformation( + // TODO(tolo): remove this ignore and migrate the code + // https://github.com/flutter/flutter/issues/124045. + // ignore: deprecated_member_use location: uri.toString(), state: this, );