From 77e95ca9ec21c9324e9b575c1352f6c629f01062 Mon Sep 17 00:00:00 2001 From: Chun-Heng Tai Date: Tue, 5 Dec 2023 21:36:03 -0800 Subject: [PATCH] update --- packages/go_router/lib/src/builder.dart | 601 ++++++------------ packages/go_router/lib/src/configuration.dart | 1 + packages/go_router/lib/src/delegate.dart | 135 ++-- packages/go_router/lib/src/match.dart | 190 +++--- .../go_router/lib/src/pages/cupertino.dart | 4 +- .../go_router/lib/src/pages/material.dart | 4 +- packages/go_router/lib/src/route.dart | 34 +- packages/go_router/test/builder_test.dart | 83 ++- packages/go_router/test/delegate_test.dart | 32 +- packages/go_router/test/go_router_test.dart | 48 +- packages/go_router/test/test_helpers.dart | 8 - 11 files changed, 478 insertions(+), 662 deletions(-) diff --git a/packages/go_router/lib/src/builder.dart b/packages/go_router/lib/src/builder.dart index e3e116fe758c..7cbbdcba2e54 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'; @@ -23,6 +22,19 @@ typedef GoRouterBuilderWithNav = Widget Function( Widget child, ); +typedef _PageBuilderForAppType = Page Function({ + required LocalKey key, + required String? name, + required Object? arguments, + required String restorationId, + required Widget child, +}); + +typedef _ErrorBuilderForAppType = Widget Function( + BuildContext context, + GoRouterState state, +); + /// 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. @@ -32,7 +44,7 @@ typedef GoRouterBuilderWithNav = Widget Function( /// /// Used by of [RouteBuilder.onPopPageWithRouteMatch]. typedef PopPageWithRouteMatchCallback = bool Function( - Route route, dynamic result, RouteMatch? match); + Route route, dynamic result, RouteMatchBase match); /// Builds the top-level Navigator for GoRouter. class RouteBuilder { @@ -74,8 +86,6 @@ 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`. /// @@ -84,13 +94,6 @@ class RouteBuilder { /// If this method returns false, this builder aborts the pop. final PopPageWithRouteMatchCallback onPopPageWithRouteMatch; - /// 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>{}; - /// Builds the top-level Navigator for the given [RouteMatchList]. Widget build( BuildContext context, @@ -103,293 +106,133 @@ class RouteBuilder { return const SizedBox.shrink(); } assert( - matchList.isError || !(matchList.last.route as GoRoute).redirectOnly); + matchList.isError || !matchList.last.route.redirectOnly); return builderWithNav( context, - Builder( - builder: (BuildContext context) { - final Map, GoRouterState> newRegistry = - , GoRouterState>{}; - final Widget result = tryBuild(context, matchList, routerNeglect, - configuration.navigatorKey, newRegistry); - _registry.updateRegistry(newRegistry); - return GoRouterStateRegistryScope(registry: _registry, child: result); - }, - ), - ); - } - - /// Builds the top-level Navigator by invoking the build method on each - /// matching route. - /// - /// Throws a [_RouteBuilderError]. - @visibleForTesting - Widget tryBuild( - BuildContext context, - RouteMatchList matchList, - bool routerNeglect, - 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( - context, - _buildNavigator( - pagePopContext.onPopPage, - _buildPages(context, matchList, pagePopContext, routerNeglect, - navigatorKey, registry), - navigatorKey, + _CustomNavigator( + navigatorKey: configuration.navigatorKey, observers: observers, - restorationScopeId: restorationScopeId, - requestFocus: requestFocus, - ), + navigatorRestorationId: restorationScopeId, + onPopPageWithRouteMatch: onPopPageWithRouteMatch, + matchList: matchList, + matches: matchList.matches, + configuration: configuration, + errorBuilder: errorBuilder, + errorPageBuilder: errorPageBuilder, + ) ); } +} - /// Returns the top-level pages instead of the root navigator. Used for - /// testing. - List> _buildPages( - BuildContext context, - RouteMatchList matchList, - _PagePopContext pagePopContext, - bool routerNeglect, - GlobalKey navigatorKey, - Map, GoRouterState> registry) { - final Map, List>> keyToPage; - if (matchList.isError) { - keyToPage = , List>>{ - navigatorKey: >[ - _buildErrorPage(context, _buildErrorState(matchList)), - ] - }; - } else { - keyToPage = , List>>{}; - _buildRecursive(context, matchList, 0, pagePopContext, routerNeglect, - keyToPage, navigatorKey, registry); +class _CustomNavigator extends StatefulWidget { + const _CustomNavigator({ + super.key, + required this.navigatorKey, + required this.observers, + required this.navigatorRestorationId, + required this.onPopPageWithRouteMatch, + required this.matchList, + required this.matches, + required this.configuration, + required this.errorBuilder, + required this.errorPageBuilder, + }); - // Every Page should have a corresponding RouteMatch. - assert(keyToPage.values.flattened.every((Page page) => - pagePopContext.getRouteMatchesForPage(page) != null)); - } + final GlobalKey navigatorKey; + final List observers; + final List matches; + final RouteMatchList matchList; + final RouteConfiguration configuration; + final PopPageWithRouteMatchCallback onPopPageWithRouteMatch; + final String? navigatorRestorationId; + final GoRouterWidgetBuilder? errorBuilder; + final GoRouterPageBuilder? errorPageBuilder; - /// Clean up previous cache to prevent memory leak, making sure any nested - /// stateful shell routes for the current match list are kept. - final Set activeKeys = keyToPage.keys.toSet() - ..addAll(_nestedStatefulNavigatorKeys(matchList)); - _goHeroCache.removeWhere( - (GlobalKey key, _) => !activeKeys.contains(key)); - return keyToPage[navigatorKey]!; - } + @override + State createState() => _CustomNavigatorState(); + +} - static Set> _nestedStatefulNavigatorKeys( - RouteMatchList matchList) { - final StatefulShellRoute? shellRoute = - matchList.routes.whereType().firstOrNull; - if (shellRoute == null) { - return >{}; +class _CustomNavigatorState extends State<_CustomNavigator> { + late final HeroController _controller; + late Map, RouteMatchBase> _pageToRouteMatchBase; + final GoRouterStateRegistry _registry = GoRouterStateRegistry(); + List>? _pages; + + @override + void didUpdateWidget(_CustomNavigator oldWidget) { + super.didUpdateWidget(oldWidget); + if (widget.matchList != oldWidget.matchList) { + _pages = null; } - return RouteBase.routesRecursively([shellRoute]) - .whereType() - .expand((StatefulShellRoute e) => - e.branches.map((StatefulShellBranch b) => b.navigatorKey)) - .toSet(); } - void _buildRecursive( - BuildContext context, - RouteMatchList matchList, - int startIndex, - _PagePopContext pagePopContext, - bool routerNeglect, - Map, List>> keyToPages, - GlobalKey navigatorKey, - Map, GoRouterState> registry, - ) { - if (startIndex >= matchList.matches.length) { - return; - } - final RouteMatch match = matchList.matches[startIndex]; - - final RouteBase route = match.route; - final GoRouterState state = buildState(matchList, match); - Page? page; - if (state.error != null) { - page = _buildErrorPage(context, state); - keyToPages.putIfAbsent(navigatorKey, () => >[]).add(page); - _buildRecursive(context, matchList, startIndex + 1, pagePopContext, - routerNeglect, keyToPages, navigatorKey, registry); + @override + void didChangeDependencies() { + super.didChangeDependencies(); + /// Return a HeroController based on the app type. + if (isMaterialApp(context)) { + _controller = createMaterialHeroController(); + } else if (isCupertinoApp(context)) { + _controller = createCupertinoHeroController(); } else { - // If this RouteBase is for a different Navigator, add it to the - // list of out of scope pages - final GlobalKey routeNavKey = - route.parentNavigatorKey ?? navigatorKey; - if (route is GoRoute) { - page = - _buildPageForGoRoute(context, state, match, route, pagePopContext); - assert(page != null || route.redirectOnly); - if (page != null) { - keyToPages - .putIfAbsent(routeNavKey, () => >[]) - .add(page); - } - - _buildRecursive(context, matchList, startIndex + 1, pagePopContext, - routerNeglect, keyToPages, navigatorKey, registry); - } else if (route is ShellRouteBase) { - assert(startIndex + 1 < matchList.matches.length, - 'Shell routes must always have child routes'); - - // Add an entry for the parent navigator if none exists. - // - // 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.putIfAbsent(routeNavKey, () => >[]).length; - - // Find the the navigator key for the sub-route of this shell route. - final RouteBase subRoute = matchList.matches[startIndex + 1].route; - final GlobalKey shellNavigatorKey = - route.navigatorKeyForSubRoute(subRoute); - - keyToPages.putIfAbsent(shellNavigatorKey, () => >[]); - - // Build the remaining pages - _buildRecursive(context, matchList, startIndex + 1, pagePopContext, - routerNeglect, keyToPages, shellNavigatorKey, registry); - - final HeroController heroController = _goHeroCache.putIfAbsent( - shellNavigatorKey, () => _getHeroController(context)); - - // Build the Navigator for this shell route - Widget buildShellNavigator( - List? observers, - String? restorationScopeId, { - bool requestFocus = true, - }) { - return _buildNavigator( - pagePopContext.onPopPage, - keyToPages[shellNavigatorKey]!, - shellNavigatorKey, - observers: observers ?? const [], - restorationScopeId: restorationScopeId, - heroController: heroController, - requestFocus: requestFocus, - ); - } - - // Call the ShellRouteBase to create/update the shell route state - final ShellRouteContext shellRouteContext = ShellRouteContext( - route: route, - routerState: state, - navigatorKey: shellNavigatorKey, - routeMatchList: matchList, - navigatorBuilder: buildShellNavigator, - ); - - // Build the Page for this route - page = _buildPageForShellRoute( - context, state, match, route, pagePopContext, shellRouteContext); - // Place the ShellRoute's Page onto the list for the parent navigator. - keyToPages[routeNavKey]!.insert(shellPageIdx, page); - } - } - if (page != null) { - registry[page] = state; - // Insert the route match in reverse order. - pagePopContext._insertRouteMatchAtStartForPage(page, match); + _controller = HeroController(); } } - static Widget _buildNavigator( - PopPageCallback onPopPage, - List> pages, - Key? navigatorKey, { - List observers = const [], - String? restorationScopeId, - HeroController? heroController, - bool requestFocus = true, - }) { - final Widget navigator = Navigator( - key: navigatorKey, - restorationScopeId: restorationScopeId, - pages: pages, - observers: observers, - onPopPage: onPopPage, - requestFocus: requestFocus, - ); - if (heroController != null) { - return HeroControllerScope( - controller: heroController, - child: navigator, - ); + void _updatePages(BuildContext context) { + assert(_pages == null); + final List> pages = >[]; + final Map, RouteMatchBase> pageToRouteMatchBase = , RouteMatchBase>{}; + final Map, GoRouterState> registry = , GoRouterState>{}; + if (widget.matchList.isError) { + pages.add(_buildErrorPage(context)); } else { - return navigator; + for (final RouteMatchBase match in widget.matches) { + final Page? page = _buildPage(context, match); + if (page == null) { + continue; + } + pages.add(page); + pageToRouteMatchBase[page] = match; + registry[page] = + match.buildState(widget.configuration, widget.matchList); + } } + _pages = pages; + _registry.updateRegistry(registry); + _pageToRouteMatchBase = pageToRouteMatchBase; } - /// Helper method that builds a [GoRouterState] object for the given [match] - /// and [pathParameters]. - @visibleForTesting - GoRouterState buildState(RouteMatchList matchList, RouteMatch match) { - final RouteBase route = match.route; - String? name; - String path = ''; - if (route is GoRoute) { - name = route.name; - path = route.path; + Page? _buildPage(BuildContext context, RouteMatchBase match) { + if (match is RouteMatch) { + return _buildPageForGoRoute(context, match); } - final RouteMatchList effectiveMatchList; - if (match is ImperativeRouteMatch) { - effectiveMatchList = match.matches; - if (effectiveMatchList.isError) { - return _buildErrorState(effectiveMatchList); - } - } else { - effectiveMatchList = matchList; - assert(!effectiveMatchList.isError); + if (match is ShellRouteMatch) { + return _buildPageForShellRoute(context, match); } - return GoRouterState( - configuration, - uri: effectiveMatchList.uri, - matchedLocation: match.matchedLocation, - name: name, - path: path, - fullPath: effectiveMatchList.fullPath, - pathParameters: - Map.from(effectiveMatchList.pathParameters), - error: effectiveMatchList.error, - extra: effectiveMatchList.extra, - pageKey: match.pageKey, - ); + throw GoError('unknown match type ${match.runtimeType}'); } + + /// Builds a [Page] for [GoRoute] - Page? _buildPageForGoRoute(BuildContext context, GoRouterState state, - RouteMatch match, GoRoute route, _PagePopContext pagePopContext) { - // Call the pageBuilder if it's non-null - final GoRouterPageBuilder? pageBuilder = route.pageBuilder; + Page? _buildPageForGoRoute(BuildContext context, RouteMatch match) { + final GoRouterPageBuilder? pageBuilder = match.route.pageBuilder; + final GoRouterState state = match.buildState(widget.configuration, widget.matchList); if (pageBuilder != null) { final Page page = pageBuilder(context, state); if (page is! NoOpPage) { return page; } } - return _callGoRouteBuilder(context, state, route); - } - /// Calls the user-provided route builder from the [GoRoute]. - Page? _callGoRouteBuilder( - BuildContext context, GoRouterState state, GoRoute route) { - final GoRouterWidgetBuilder? builder = route.builder; + final GoRouterWidgetBuilder? builder = match.route.builder; if (builder == null) { return null; } - return buildPage(context, state, Builder(builder: (BuildContext context) { + return _buildPlatformAdapterPage(context, state, Builder(builder: (BuildContext context) { return builder(context, state); })); } @@ -397,44 +240,46 @@ class RouteBuilder { /// 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; + ShellRouteMatch match, + ) { + final GoRouterState state = match.buildState(widget.configuration, widget.matchList); + final GlobalKey navigatorKey = match.navigatorKey; + final ShellRouteContext shellRouteContext = ShellRouteContext( + route: match.route, + routerState: state, + navigatorKey: navigatorKey, + routeMatchList: widget.matchList, + navigatorBuilder: (List? observers, String? restorationScopeId) { + return _CustomNavigator( + // The state needs to persist across rebuild. + key: GlobalObjectKey(navigatorKey.hashCode), + navigatorRestorationId: restorationScopeId, + navigatorKey: navigatorKey, + matches: match.matches, + matchList: widget.matchList, + configuration: widget.configuration, + observers: observers ?? const [], + onPopPageWithRouteMatch: widget.onPopPageWithRouteMatch, + // This is used to recursively build pages under this shell route. + errorBuilder: widget.errorBuilder, + errorPageBuilder: widget.errorPageBuilder, + ); + }, + ); + final Page? page = match.route.buildPage(context, state, shellRouteContext); + if (page != null && page is! NoOpPage) { + return page; } // 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) { - assert(shellRouteContext != null, - 'ShellRouteContext must be provided for ${route.runtimeType}'); - final Widget? widget = - route.buildWidget(context, state, shellRouteContext!); - if (widget == null) { - throw GoError('No builder provided to ShellRoute: $route'); - } - - return widget; + return _buildPlatformAdapterPage(context, state, Builder(builder: (BuildContext context) { + return match.route.buildWidget(context, state, shellRouteContext)!; + })); } _PageBuilderForAppType? _pageBuilderForAppType; - Widget Function( - BuildContext context, - GoRouterState state, - )? _errorBuilderForAppType; + _ErrorBuilderForAppType? _errorBuilderForAppType; void _cacheAppType(BuildContext context) { // cache app type-specific page and error builders @@ -456,7 +301,20 @@ class RouteBuilder { (BuildContext c, GoRouterState s) => CupertinoErrorScreen(s.error); } else { log('Using WidgetsApp configuration'); - _pageBuilderForAppType = pageBuilderForWidgetApp; + _pageBuilderForAppType = ({ + required LocalKey key, + required String? name, + required Object? arguments, + required String restorationId, + required Widget child, + }) => + NoTransitionPage( + name: name, + arguments: arguments, + key: key, + restorationId: restorationId, + child: child, + ); _errorBuilderForAppType = (BuildContext c, GoRouterState s) => ErrorScreen(s.error); } @@ -467,12 +325,11 @@ class RouteBuilder { } /// builds the page based on app type, i.e. MaterialApp vs. CupertinoApp - @visibleForTesting - Page buildPage( - BuildContext context, - GoRouterState state, - Widget child, - ) { + Page _buildPlatformAdapterPage( + BuildContext context, + GoRouterState state, + Widget child, + ) { // build the page based on app type _cacheAppType(context); return _pageBuilderForAppType!( @@ -487,37 +344,22 @@ class RouteBuilder { ); } - /// Builds a page without any transitions. - Page pageBuilderForWidgetApp({ - required LocalKey key, - required String? name, - required Object? arguments, - required String restorationId, - required Widget child, - }) => - NoTransitionPage( - name: name, - arguments: arguments, - key: key, - restorationId: restorationId, - child: child, - ); - - GoRouterState _buildErrorState(RouteMatchList matchList) { - assert(matchList.isError); + GoRouterState _buildErrorState() { + assert(widget.matchList.isError); return GoRouterState( - configuration, - uri: matchList.uri, - matchedLocation: matchList.uri.path, - fullPath: matchList.fullPath, - pathParameters: matchList.pathParameters, - error: matchList.error, - pageKey: ValueKey('${matchList.uri}(error)'), + widget.configuration, + uri: widget.matchList.uri, + matchedLocation: widget.matchList.uri.path, + fullPath: widget.matchList.fullPath, + pathParameters: widget.matchList.pathParameters, + error: widget.matchList.error, + pageKey: ValueKey('${widget.matchList.uri}(error)'), ); } /// Builds a an error page. - Page _buildErrorPage(BuildContext context, GoRouterState state) { + Page _buildErrorPage(BuildContext context) { + final GoRouterState state = _buildErrorState(); assert(state.error != null); // If the error page builder is provided, use that, otherwise, if the error @@ -525,78 +367,53 @@ class RouteBuilder { // MaterialPage). Finally, if nothing is provided, use a default error page // wrapped in the app-specific page. _cacheAppType(context); - final GoRouterWidgetBuilder? errorBuilder = this.errorBuilder; - return errorPageBuilder != null - ? errorPageBuilder!(context, state) - : buildPage( - context, - state, - errorBuilder != null - ? errorBuilder(context, state) - : _errorBuilderForAppType!(context, state), - ); + final GoRouterWidgetBuilder? errorBuilder = widget.errorBuilder; + return widget.errorPageBuilder != null + ? widget.errorPageBuilder!(context, state) + : _buildPlatformAdapterPage( + context, + state, + errorBuilder != null + ? errorBuilder(context, state) + : _errorBuilderForAppType!(context, state), + ); } - /// Return a HeroController based on the app type. - HeroController _getHeroController(BuildContext context) { - if (context is Element) { - if (isMaterialApp(context)) { - return createMaterialHeroController(); - } else if (isCupertinoApp(context)) { - return createCupertinoHeroController(); - } - } - return HeroController(); + @override + void dispose() { + _controller.dispose(); + super.dispose(); } -} -typedef _PageBuilderForAppType = Page Function({ - required LocalKey key, - required String? name, - required Object? arguments, - required String restorationId, - required Widget child, -}); - -/// Context used to provide a route to page association when popping routes. -class _PagePopContext { - _PagePopContext._(this.onPopPageWithRouteMatch); - - /// A page can be mapped to a RouteMatch list, such as a const page being - /// pushed multiple times. - final Map, List> _routeMatchesLookUp = - , List>{}; - - /// On pop page callback that includes the associated [RouteMatch]. - final PopPageWithRouteMatchCallback onPopPageWithRouteMatch; - - /// 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. - List? getRouteMatchesForPage(Page page) => - _routeMatchesLookUp[page]; - - /// This is called in _buildRecursive to insert route matches in reverse order. - void _insertRouteMatchAtStartForPage(Page page, RouteMatch match) { - _routeMatchesLookUp - .putIfAbsent(page, () => []) - .insert(0, match); + bool _handlePopPage( + Route route, Object? result) { + final Page page = route.settings as Page; + final RouteMatchBase match = _pageToRouteMatchBase[page]!; + return widget.onPopPageWithRouteMatch(route, result, match); } - /// Function used as [Navigator.onPopPage] callback when creating Navigators. - /// - /// This function forwards to [onPopPageWithRouteMatch], including the - /// [RouteMatch] associated with the popped route. - /// - /// This assumes always pop the last route match for the page. - bool onPopPage(Route route, dynamic result) { - final Page page = route.settings as Page; - final RouteMatch match = _routeMatchesLookUp[page]!.last; - if (onPopPageWithRouteMatch(route, result, match)) { - _routeMatchesLookUp[page]!.removeLast(); - return true; + @override + Widget build(BuildContext context) { + if (_pages == null) { + _updatePages(context); } - return false; + assert(_pages != null); + return GoRouterStateRegistryScope( + registry: _registry, + child: HeroControllerScope( + controller: _controller, + child: Builder( + builder: (BuildContext context) { + return Navigator( + key: widget.navigatorKey, + restorationScopeId: widget.navigatorRestorationId, + pages: _pages!, + observers: widget.observers, + onPopPage: _handlePopPage, + ); + }, + ), + ), + ); } } diff --git a/packages/go_router/lib/src/configuration.dart b/packages/go_router/lib/src/configuration.dart index 3d65c2ecd8ab..734e1646c4fb 100644 --- a/packages/go_router/lib/src/configuration.dart +++ b/packages/go_router/lib/src/configuration.dart @@ -399,6 +399,7 @@ class RouteConfiguration { if (match is RouteMatch) { routeMatches.add(match); } + return true; }); final FutureOr routeLevelRedirectResult = diff --git a/packages/go_router/lib/src/delegate.dart b/packages/go_router/lib/src/delegate.dart index 4026903d30bf..dbec694adde1 100644 --- a/packages/go_router/lib/src/delegate.dart +++ b/packages/go_router/lib/src/delegate.dart @@ -50,21 +50,27 @@ class GoRouterDelegate extends RouterDelegate final RouteConfiguration _configuration; - _NavigatorStateIterator _createNavigatorStateIterator() => - _NavigatorStateIterator(currentConfiguration, navigatorKey.currentState!); - @override Future popRoute() async { - final _NavigatorStateIterator iterator = _createNavigatorStateIterator(); - while (iterator.moveNext()) { - final bool didPop = await iterator.current.maybePop(); - if (didPop) { - return true; + NavigatorState? state = navigatorKey.currentState; + if (state == null) { + return false; + } + if (!state.canPop()) { + state = null; + } + RouteMatchBase walker = currentConfiguration.matches.last; + while (walker is ShellRouteMatch) { + if (walker.navigatorKey.currentState?.canPop() ?? false) { + state = walker.navigatorKey.currentState; } + walker = walker.matches.last; + } + if (state != null) { + return state.maybePop(); } // This should be the only place where the last GoRoute exit the screen. - final GoRoute lastRoute = - currentConfiguration.matches.last.route as GoRoute; + final GoRoute lastRoute = currentConfiguration.last.route; if (lastRoute.onExit != null && navigatorKey.currentContext != null) { return !(await lastRoute.onExit!(navigatorKey.currentContext!)); } @@ -73,25 +79,36 @@ class GoRouterDelegate extends RouterDelegate /// Returns `true` if the active Navigator can pop. bool canPop() { - final _NavigatorStateIterator iterator = _createNavigatorStateIterator(); - while (iterator.moveNext()) { - if (iterator.current.canPop()) { + if (navigatorKey.currentState?.canPop() ?? false) { + return true; + } + RouteMatchBase walker = currentConfiguration.matches.last; + while (walker is ShellRouteMatch) { + if (walker.navigatorKey.currentState?.canPop() ?? false) { return true; } + walker = walker.matches.last; } return false; } /// Pops the top-most route. void pop([T? result]) { - final _NavigatorStateIterator iterator = _createNavigatorStateIterator(); - while (iterator.moveNext()) { - if (iterator.current.canPop()) { - iterator.current.pop(result); - return; + NavigatorState? state; + if (navigatorKey.currentState?.canPop() ?? false) { + state = navigatorKey.currentState; + } + RouteMatchBase walker = currentConfiguration.matches.last; + while (walker is ShellRouteMatch) { + if (walker.navigatorKey.currentState?.canPop() ?? false) { + state = walker.navigatorKey.currentState; } + walker = walker.matches.last; } - throw GoError('There is nothing to pop'); + if (state == null) { + throw GoError('There is nothing to pop'); + } + state.pop(result); } void _debugAssertMatchListNotEmpty() { @@ -103,14 +120,13 @@ class GoRouterDelegate extends RouterDelegate } bool _handlePopPageWithRouteMatch( - Route route, Object? result, RouteMatch? match) { + Route route, Object? result, RouteMatchBase match) { if (route.willHandlePopInternally) { final bool popped = route.didPop(result); assert(!popped); return popped; } - assert(match != null); - final RouteBase routeBase = match!.route; + final RouteBase routeBase = match.route; if (routeBase is! GoRoute || routeBase.onExit == null) { route.didPop(result); _completeRouteMatch(result, match); @@ -130,7 +146,7 @@ class GoRouterDelegate extends RouterDelegate return false; } - void _completeRouteMatch(Object? result, RouteMatch match) { + void _completeRouteMatch(Object? result, RouteMatchBase match) { if (match is ImperativeRouteMatch) { match.complete(result); } @@ -178,12 +194,14 @@ class GoRouterDelegate extends RouterDelegate if (match is RouteMatch) { currentGoRouteMatches.add(match); } + return true; }); final List newGoRouteMatches = []; configuration.visitRouteMatches((RouteMatchBase match) { if (match is RouteMatch) { newGoRouteMatches.add(match); } + return true; }); final int compareUntil = math.min( @@ -250,77 +268,8 @@ class GoRouterDelegate extends RouterDelegate Future _setCurrentConfiguration(RouteMatchList configuration) { currentConfiguration = configuration; + print('configuration set to:\n$configuration'); notifyListeners(); return SynchronousFuture(null); } } - -/// An iterator that iterates through navigators that [GoRouterDelegate] -/// created from the inner to outer. -/// -/// The iterator starts with the navigator that hosts the top-most route. This -/// navigator may not be the inner-most navigator if the top-most route is a -/// pageless route, such as a dialog or bottom sheet. -class _NavigatorStateIterator implements Iterator { - _NavigatorStateIterator(this.matchList, this.root) - : index = matchList.matches.length - 1; - - final RouteMatchList matchList; - int index; - - final NavigatorState root; - @override - late NavigatorState current; - - RouteBase _getRouteAtIndex(int index) => matchList.matches[index].route; - - void _findsNextIndex() { - final GlobalKey? parentNavigatorKey = - _getRouteAtIndex(index).parentNavigatorKey; - if (parentNavigatorKey == null) { - index -= 1; - return; - } - - for (index -= 1; index >= 0; index -= 1) { - final RouteBase route = _getRouteAtIndex(index); - if (route is ShellRouteBase) { - if (route.navigatorKeyForSubRoute(_getRouteAtIndex(index + 1)) == - parentNavigatorKey) { - return; - } - } - } - assert(root == parentNavigatorKey.currentState); - } - - @override - bool moveNext() { - if (index < 0) { - return false; - } - _findsNextIndex(); - - while (index >= 0) { - final RouteBase route = _getRouteAtIndex(index); - if (route is ShellRouteBase) { - final GlobalKey navigatorKey = - route.navigatorKeyForSubRoute(_getRouteAtIndex(index + 1)); - // Must have a ModalRoute parent because the navigator ShellRoute - // created must not be the root navigator. - final ModalRoute parentModalRoute = - ModalRoute.of(navigatorKey.currentContext!)!; - // There may be pageless route on top of ModalRoute that the - // parentNavigatorKey is in. For example an open dialog. - if (parentModalRoute.isCurrent) { - current = navigatorKey.currentState!; - return true; - } - } - _findsNextIndex(); - } - assert(index == -1); - current = root; - return true; - } -} diff --git a/packages/go_router/lib/src/match.dart b/packages/go_router/lib/src/match.dart index 8033a6035655..bb0f283b9373 100644 --- a/packages/go_router/lib/src/match.dart +++ b/packages/go_router/lib/src/match.dart @@ -19,17 +19,32 @@ import 'route.dart'; import 'state.dart'; /// The function signature for [RouteMatchList.visitRouteMatches] +/// +/// Return false to stop the walk. @internal -typedef RouteMatchVisitor = void Function(RouteMatchBase); +typedef RouteMatchVisitor = bool Function(RouteMatchBase); /// The base class for various route matches. -abstract class RouteMatchBase { +abstract class RouteMatchBase with Diagnosticable { /// An abstract route match base const RouteMatchBase(); /// The matched route. RouteBase get route; + /// The page key. + ValueKey get pageKey; + + /// The location string that matches the [route]. + /// + /// for example: + /// + /// uri = '/family/f2/person/p2' + /// route = GoRoute('/family/:id') + /// + /// matchedLocation = '/family/f2' + String get matchedLocation; + /// Gets the state that represent this route match. GoRouterState buildState(RouteConfiguration configuration, RouteMatchList matches); @@ -111,7 +126,9 @@ abstract class RouteMatchBase { }) { final GlobalKey? parentKey = route.parentNavigatorKey == null || route.parentNavigatorKey == scopedNavigatorKey ? null : route.parentNavigatorKey; Map?, List>? subRouteMatches; + late GlobalKey navigatorKeyUsed; for (final RouteBase subRoute in route.routes) { + navigatorKeyUsed = route.navigatorKeyForSubRoute(subRoute); subRouteMatches = _matchByNavigatorKey( route: subRoute, matchedPath: matchedPath, @@ -119,7 +136,7 @@ abstract class RouteMatchBase { matchedLocation: matchedLocation, pathParameters: pathParameters, uri: uri, - scopedNavigatorKey: route.navigatorKeyForSubRoute(subRoute), + scopedNavigatorKey: navigatorKeyUsed, ); assert(!subRouteMatches.containsKey(route.navigatorKeyForSubRoute(subRoute))); if (subRouteMatches.isNotEmpty) { @@ -129,7 +146,6 @@ abstract class RouteMatchBase { if (subRouteMatches?.isEmpty ?? true) { return _empty; } - final RouteMatchBase result = ShellRouteMatch( route: route, // The RouteConfiguration should have asserted the subRouteMatches must @@ -137,6 +153,7 @@ abstract class RouteMatchBase { matches: subRouteMatches!.remove(null)!, matchedLocation: remainingLocation, pageKey: ValueKey(route.hashCode.toString()), + navigatorKey: navigatorKeyUsed, ); return ?, List>{ @@ -169,8 +186,8 @@ abstract class RouteMatchBase { final String newMatchedLocation = concatenatePaths(matchedLocation, pathLoc); final String newMatchedPath = concatenatePaths(matchedPath, route.path); - - if (newMatchedLocation == uri.path) { + final String lowCasePath = uri.path.toLowerCase(); + if (newMatchedLocation == lowCasePath) { // A complete match. pathParameters.addAll(currentPathParameter); @@ -184,10 +201,10 @@ abstract class RouteMatchBase { ], }; } - assert(uri.path.startsWith(newMatchedLocation)); + assert(lowCasePath.startsWith(newMatchedLocation)); assert(remainingLocation.isNotEmpty); - final String childRestLoc = uri.path.substring(newMatchedLocation.length + + final String childRestLoc = lowCasePath.substring(newMatchedLocation.length + (newMatchedLocation == '/' ? 0 : 1)); Map?, List>? subRouteMatches; @@ -219,6 +236,12 @@ abstract class RouteMatchBase { )); return subRouteMatches; } + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties.add(DiagnosticsProperty('route', route)); + } } /// An matched result by matching a [RouteBase] against a location. @@ -237,20 +260,12 @@ class RouteMatch extends RouteMatchBase { @override final GoRoute route; - /// The location string that matches the [route]. - /// - /// for example: - /// - /// uri = '/family/f2/person/p2' - /// route = GoRoute('/family/:id') - /// - /// matchedLocation = '/family/f2' + @override final String matchedLocation; - /// The page key. + @override final ValueKey pageKey; - @override bool operator ==(Object other) { if (other.runtimeType != runtimeType) { @@ -273,13 +288,15 @@ class RouteMatch extends RouteMatchBase { matchedLocation: matchedLocation, fullPath: matches.fullPath, pathParameters: matches.pathParameters, - pageKey: pageKey + pageKey: pageKey, + name: route.name, + path: route.path, + extra: matches.extra, ); } } /// The route match that represent route pushed through [GoRouter.push]. - @immutable class ShellRouteMatch extends RouteMatchBase { /// Create a match. @@ -288,6 +305,7 @@ class ShellRouteMatch extends RouteMatchBase { required this.matches, required this.matchedLocation, required this.pageKey, + required this.navigatorKey, }) : assert(matches.isNotEmpty); @override @@ -301,20 +319,16 @@ class ShellRouteMatch extends RouteMatchBase { return currentMatch as RouteMatch; } - /// The location string that matches the [route]. - /// - /// for example: - /// - /// uri = '/family/f2/person/p2' - /// route = GoRoute('/family/:id') - /// - /// matchedLocation = '/family/f2' + /// The navigator key used for this match. + final GlobalKey navigatorKey; + + @override final String matchedLocation; /// The matches that will be built under this shell route. final List matches; - /// The page key. + @override final ValueKey pageKey; @override @@ -330,18 +344,22 @@ class ShellRouteMatch extends RouteMatchBase { matchedLocation: matchedLocation, fullPath: matches.fullPath, pathParameters: matches.pathParameters, - pageKey: pageKey + pageKey: pageKey, + extra: matches.extra, ); } - ShellRouteMatch _copyWith({ - List? matches, + /// Creates a new shell route match with the given matches. + @internal + ShellRouteMatch copyWith({ + required List? matches, }) { return ShellRouteMatch( matches: matches ?? this.matches, route: route, matchedLocation: matchedLocation, pageKey: pageKey, + navigatorKey: navigatorKey, ); } @@ -356,6 +374,12 @@ class ShellRouteMatch extends RouteMatchBase { @override int get hashCode => Object.hash(route, matchedLocation, Object.hashAll(matches), pageKey); + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties.add(DiagnosticsProperty>('matches', matches)); + } } /// The route match that represent route pushed through [GoRouter.push]. @@ -397,14 +421,7 @@ class ImperativeRouteMatch extends RouteMatch { @override GoRouterState buildState(RouteConfiguration configuration, RouteMatchList matches) { - return GoRouterState( - configuration, - uri: this.matches.uri, - matchedLocation: matchedLocation, - fullPath: this.matches.fullPath, - pathParameters: this.matches.pathParameters, - pageKey: pageKey - ); + return super.buildState(configuration, this.matches); } @override @@ -424,7 +441,7 @@ class ImperativeRouteMatch extends RouteMatch { /// /// This corresponds to the GoRouter's history. @immutable -class RouteMatchList { +class RouteMatchList with Diagnosticable { /// RouteMatchList constructor. RouteMatchList({ required this.matches, @@ -531,43 +548,47 @@ class RouteMatchList { /// Returns a new instance of RouteMatchList with the input `match` pushed /// onto the current instance. RouteMatchList push(ImperativeRouteMatch match) { - // Compares two RouteMatchList and finds the first branch that is - // incompatible (produce by different ShellRouteBase) from the back of the - // list. - List newMatches = matches.toList(); - // The newMatches will be used as walker later, create the result with current - // reference first. - final RouteMatchList result = _copyWith(matches: newMatches); - List otherMatches = match.matches.matches; - while(otherMatches.last is ShellRouteMatch && otherMatches.last.route == otherMatches.last.route) { + return copyWith(matches: _createNewMatchUntilIncompatible(matches, match.matches.matches, match)); + } + + static List _createNewMatchUntilIncompatible( + List currentMatches, + List otherMatches, + ImperativeRouteMatch match, + ) { + final List newMatches = currentMatches.toList(); + if (otherMatches.last is ShellRouteMatch && otherMatches.last.route == newMatches.last.route) { assert(newMatches.last is ShellRouteMatch); final ShellRouteMatch lastShellRouteMatch = newMatches.removeLast() as ShellRouteMatch; - newMatches.add(lastShellRouteMatch._copyWith(matches: lastShellRouteMatch.matches.toList())); - newMatches = lastShellRouteMatch.matches; - otherMatches = (otherMatches.last as ShellRouteMatch).matches; + newMatches.add( + // Create a new copy of the `lastShellRouteMatch`. + lastShellRouteMatch.copyWith( + matches: _createNewMatchUntilIncompatible(lastShellRouteMatch.matches, (otherMatches.last as ShellRouteMatch).matches, match), + ), + ); + return newMatches; } + newMatches.add(_cloneBranchAndInsertImperativeMatch(otherMatches.last, match)); + return newMatches; + } - // Insert the last branch to the end of newMatches. - RouteMatchBase incompatibleMatch = otherMatches.last; - while (incompatibleMatch is ShellRouteMatch) { - final ShellRouteMatch newBranch = incompatibleMatch._copyWith(matches: []); - newMatches.add(newBranch); - newMatches = newBranch.matches; - incompatibleMatch = incompatibleMatch.matches.last; + static RouteMatchBase _cloneBranchAndInsertImperativeMatch(RouteMatchBase branch, ImperativeRouteMatch match) { + if (branch is ShellRouteMatch) { + return branch.copyWith( + matches: [ + _cloneBranchAndInsertImperativeMatch(branch.matches.last, match), + ], + ); } - - assert(incompatibleMatch.route == match.route); // Add the input `match` instead of the incompatibleMatch since it contains - // pagekey and push future. - newMatches.add(match); - - // The reference in result should have been updated. - return result; + // page key and push future. + assert(branch.route == match.route); + return match; } /// Returns a new instance of RouteMatchList with the input `match` removed /// from the current instance. - RouteMatchList remove(RouteMatch match) { + RouteMatchList remove(RouteMatchBase match) { final List newMatches = _removeRouteMatchFromList(matches, match); if (newMatches == matches) { return this; @@ -575,7 +596,7 @@ class RouteMatchList { final String fullPath = _generateFullPath(newMatches); if (this.fullPath == fullPath) { - return _copyWith( + return copyWith( matches: newMatches, ); } @@ -590,7 +611,7 @@ class RouteMatchList { ); final Uri newUri = uri.replace(path: patternToPath(fullPath, newPathParameters)); - return _copyWith( + return copyWith( matches: newMatches, uri: newUri, pathParameters: newPathParameters, @@ -608,7 +629,7 @@ class RouteMatchList { /// /// If a target is found, the target and every node after the target in tree /// order is removed. - static List _removeRouteMatchFromList(List matches, RouteMatch target) { + static List _removeRouteMatchFromList(List matches, RouteMatchBase target) { // Remove is usually caused by pop; therefore, start searching from the end. for (int index = matches.length - 1; index >= 0; index -= 1) { final RouteMatchBase match = matches[index]; @@ -625,7 +646,7 @@ class RouteMatchList { return [ ...matches.sublist(0, index), if (newSubMatches.isNotEmpty) - match._copyWith(matches: newSubMatches), + match.copyWith(matches: newSubMatches), ]; } } @@ -647,7 +668,8 @@ class RouteMatchList { /// The routes for each of the matches. List get routes => matches.map((RouteMatchBase e) => e.route).toList(); - /// Traversal all route matches in this match list in preorder. + /// Traverse route matches in this match list in preorder until visitor + /// returns false. /// /// This method visit recursively into shell route matches. @internal @@ -655,16 +677,21 @@ class RouteMatchList { _visitRouteMatches(matches, visitor); } - static void _visitRouteMatches(List matches, RouteMatchVisitor visitor) { + static bool _visitRouteMatches(List matches, RouteMatchVisitor visitor) { for (final RouteMatchBase routeMatch in matches) { - visitor(routeMatch); - if (routeMatch is ShellRouteMatch) { - _visitRouteMatches(routeMatch.matches, visitor); + if (!visitor(routeMatch)) { + return false; + } + if (routeMatch is ShellRouteMatch && !_visitRouteMatches(routeMatch.matches, visitor)) { + return false; } } + return true; } - RouteMatchList _copyWith({ + /// Create a new [RouteMatchList] with given parameter replaced. + @internal + RouteMatchList copyWith({ List? matches, Uri? uri, Map? pathParameters, @@ -706,8 +733,10 @@ class RouteMatchList { } @override - String toString() { - return '${objectRuntimeType(this, 'RouteMatchList')}($fullPath)'; + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties.add(DiagnosticsProperty('uri', uri)); + properties.add(DiagnosticsProperty>('matches', matches)); } } @@ -750,6 +779,7 @@ class _RouteMatchListEncoder if (match is ImperativeRouteMatch) { imperativeMatches.add(match); } + return true; }); final List> encodedImperativeMatches = imperativeMatches .map((ImperativeRouteMatch e) => _toPrimitives( diff --git a/packages/go_router/lib/src/pages/cupertino.dart b/packages/go_router/lib/src/pages/cupertino.dart index 06a04ccf4e40..cf64d8124486 100644 --- a/packages/go_router/lib/src/pages/cupertino.dart +++ b/packages/go_router/lib/src/pages/cupertino.dart @@ -8,8 +8,8 @@ import 'package:flutter/cupertino.dart'; import '../misc/extensions.dart'; /// Checks for CupertinoApp in the widget tree. -bool isCupertinoApp(Element elem) => - elem.findAncestorWidgetOfExactType() != null; +bool isCupertinoApp(BuildContext context) => + context.findAncestorWidgetOfExactType() != null; /// Creates a Cupertino HeroController. HeroController createCupertinoHeroController() => diff --git a/packages/go_router/lib/src/pages/material.dart b/packages/go_router/lib/src/pages/material.dart index fe0f7974d58b..06fb770a78ee 100644 --- a/packages/go_router/lib/src/pages/material.dart +++ b/packages/go_router/lib/src/pages/material.dart @@ -9,8 +9,8 @@ import 'package:flutter/material.dart'; import '../misc/extensions.dart'; /// Checks for MaterialApp in the widget tree. -bool isMaterialApp(Element elem) => - elem.findAncestorWidgetOfExactType() != null; +bool isMaterialApp(BuildContext context) => + context.findAncestorWidgetOfExactType() != null; /// Creates a Material HeroController. HeroController createMaterialHeroController() => diff --git a/packages/go_router/lib/src/route.dart b/packages/go_router/lib/src/route.dart index 587905a0879b..76fb22b467d6 100644 --- a/packages/go_router/lib/src/route.dart +++ b/packages/go_router/lib/src/route.dart @@ -1222,26 +1222,24 @@ class StatefulNavigationShellState extends State /// 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 matchList.copyWith(matches: _scopeMatches(matchList.matches)); + } + + List _scopeMatches(List matches) { + final List result = []; + for (final RouteMatchBase match in matches) { + if (match is ShellRouteMatch) { + if (match.route == route) { + result.add(match); + // Discard any other route match after current shell route. + break; + } + result.add(match.copyWith(matches: _scopeMatches(match.matches))); + continue; } - return false; - }); - if (index > 0) { - final List matches = matchList.matches.sublist(0, index); - return RouteMatchList( - extra: matchList.extra, - matches: matches, - uri: Uri.parse(matches.last.matchedLocation), - pathParameters: matchList.pathParameters, - ); + result.add(match); } - return matchList; + return result; } void _updateCurrentBranchStateFromWidget() { diff --git a/packages/go_router/test/builder_test.dart b/packages/go_router/test/builder_test.dart index f1a08eef4269..061950e9de4f 100644 --- a/packages/go_router/test/builder_test.dart +++ b/packages/go_router/test/builder_test.dart @@ -49,21 +49,24 @@ void main() { }); testWidgets('Builds ShellRoute', (WidgetTester tester) async { + final GlobalKey shellNavigatorKey = GlobalKey(); final RouteConfiguration config = createRouteConfiguration( routes: [ ShellRoute( - builder: - (BuildContext context, GoRouterState state, Widget child) { - return _DetailsScreen(); - }, - routes: [ - GoRoute( - path: '/', - builder: (BuildContext context, GoRouterState state) { - return _DetailsScreen(); - }, - ), - ]), + navigatorKey: shellNavigatorKey, + builder: + (BuildContext context, GoRouterState state, Widget child) { + return _DetailsScreen(); + }, + routes: [ + GoRoute( + path: '/', + builder: (BuildContext context, GoRouterState state) { + return _DetailsScreen(); + }, + ), + ], + ), ], redirectLimit: 10, topRedirect: (BuildContext context, GoRouterState state) { @@ -73,9 +76,20 @@ void main() { ); final RouteMatchList matches = RouteMatchList( - matches: [ - createRouteMatch(config.routes.first, '/'), - createRouteMatch(config.routes.first.routes.first, '/'), + matches: [ + ShellRouteMatch( + route: config.routes.first as ShellRouteBase, + matchedLocation: '', + pageKey: const ValueKey(''), + navigatorKey: shellNavigatorKey, + matches: [ + RouteMatch( + route: config.routes.first.routes.first as GoRoute, + matchedLocation: '/', + pageKey: const ValueKey('/'), + ), + ], + ), ], uri: Uri.parse('/'), pathParameters: const {}, @@ -164,16 +178,19 @@ void main() { ); final RouteMatchList matches = RouteMatchList( - matches: [ - RouteMatch( - route: config.routes.first, + matches: [ + ShellRouteMatch( + route: config.routes.first as ShellRouteBase, matchedLocation: '', pageKey: const ValueKey(''), - ), - RouteMatch( - route: config.routes.first.routes.first, - matchedLocation: '/details', - pageKey: const ValueKey('/details'), + navigatorKey: shellNavigatorKey, + matches: [ + RouteMatch( + route: config.routes.first.routes.first as GoRoute, + matchedLocation: '/details', + pageKey: const ValueKey('/details'), + ), + ] ), ], uri: Uri.parse('/details'), @@ -290,9 +307,20 @@ void main() { ); final RouteMatchList matches = RouteMatchList( - matches: [ - createRouteMatch(config.routes.first, ''), - createRouteMatch(config.routes.first.routes.first, '/a'), + matches: [ + ShellRouteMatch( + route: config.routes.first as ShellRouteBase, + matchedLocation: '', + pageKey: const ValueKey(''), + navigatorKey: shellNavigatorKey, + matches: [ + RouteMatch( + route: config.routes.first.routes.first as GoRoute, + matchedLocation: '/a', + pageKey: const ValueKey('/a'), + ), + ], + ), ], uri: Uri.parse('/b'), pathParameters: const {}, @@ -430,8 +458,7 @@ class _BuilderTestWidget extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp( - home: builder.tryBuild(context, matches, false, - routeConfiguration.navigatorKey, , GoRouterState>{}), + home: builder.build(context, matches, false), ); } } diff --git a/packages/go_router/test/delegate_test.dart b/packages/go_router/test/delegate_test.dart index c1ece69a465c..c58a5f23587b 100644 --- a/packages/go_router/test/delegate_test.dart +++ b/packages/go_router/test/delegate_test.dart @@ -77,8 +77,8 @@ void main() { final GoRouter goRouter = await createGoRouter(tester) ..push('/error'); await tester.pumpAndSettle(); - - final RouteMatch last = + expect(find.byType(ErrorScreen), findsOneWidget); + final RouteMatchBase last = goRouter.routerDelegate.currentConfiguration.matches.last; await goRouter.routerDelegate.popRoute(); expect(goRouter.routerDelegate.currentConfiguration.matches.length, 1); @@ -194,10 +194,8 @@ void main() { await createGoRouterWithStatefulShellRoute(tester); goRouter.push('/c/c1'); await tester.pumpAndSettle(); - goRouter.push('/a'); await tester.pumpAndSettle(); - expect(goRouter.routerDelegate.currentConfiguration.matches.length, 3); expect( goRouter.routerDelegate.currentConfiguration.matches[1].pageKey, @@ -219,11 +217,13 @@ void main() { goRouter.push('/c/c2'); await tester.pumpAndSettle(); - expect(goRouter.routerDelegate.currentConfiguration.matches.length, 3); + expect(goRouter.routerDelegate.currentConfiguration.matches.length, 2); + final ShellRouteMatch shellRouteMatch = goRouter.routerDelegate.currentConfiguration.matches.last as ShellRouteMatch; + expect(shellRouteMatch.matches.length, 2); expect( - goRouter.routerDelegate.currentConfiguration.matches[1].pageKey, + shellRouteMatch.matches[0].pageKey, isNot(equals( - goRouter.routerDelegate.currentConfiguration.matches[2].pageKey)), + shellRouteMatch.matches[1].pageKey)), ); }, ); @@ -240,11 +240,13 @@ void main() { goRouter.push('/c'); await tester.pumpAndSettle(); - expect(goRouter.routerDelegate.currentConfiguration.matches.length, 3); + expect(goRouter.routerDelegate.currentConfiguration.matches.length, 2); + final ShellRouteMatch shellRouteMatch = goRouter.routerDelegate.currentConfiguration.matches.last as ShellRouteMatch; + expect(shellRouteMatch.matches.length, 2); expect( - goRouter.routerDelegate.currentConfiguration.matches[1].pageKey, + shellRouteMatch.matches[0].pageKey, isNot(equals( - goRouter.routerDelegate.currentConfiguration.matches[2].pageKey)), + shellRouteMatch.matches[1].pageKey)), ); }, ); @@ -294,7 +296,7 @@ void main() { goRouter.push('/page-0'); goRouter.routerDelegate.addListener(expectAsync0(() {})); - final RouteMatch first = + final RouteMatchBase first = goRouter.routerDelegate.currentConfiguration.matches.first; final RouteMatch last = goRouter.routerDelegate.currentConfiguration.last; goRouter.pushReplacement('/page-1'); @@ -376,7 +378,7 @@ void main() { goRouter.pushNamed('page0'); goRouter.routerDelegate.addListener(expectAsync0(() {})); - final RouteMatch first = + final RouteMatchBase first = goRouter.routerDelegate.currentConfiguration.matches.first; final RouteMatch last = goRouter.routerDelegate.currentConfiguration.last; @@ -395,7 +397,7 @@ void main() { expect( goRouter.routerDelegate.currentConfiguration.last, isA().having( - (RouteMatch match) => (match.route as GoRoute).name, + (RouteMatch match) => match.route.name, 'match.route.name', 'page1', ), @@ -425,7 +427,7 @@ void main() { goRouter.push('/page-0'); goRouter.routerDelegate.addListener(expectAsync0(() {})); - final RouteMatch first = + final RouteMatchBase first = goRouter.routerDelegate.currentConfiguration.matches.first; final RouteMatch last = goRouter.routerDelegate.currentConfiguration.last; goRouter.replace('/page-1'); @@ -546,7 +548,7 @@ void main() { goRouter.pushNamed('page0'); goRouter.routerDelegate.addListener(expectAsync0(() {})); - final RouteMatch first = + final RouteMatchBase first = goRouter.routerDelegate.currentConfiguration.matches.first; final RouteMatch last = goRouter.routerDelegate.currentConfiguration.last; goRouter.replaceNamed('page1'); diff --git a/packages/go_router/test/go_router_test.dart b/packages/go_router/test/go_router_test.dart index e338aeee65e7..a8b2bf9a243b 100644 --- a/packages/go_router/test/go_router_test.dart +++ b/packages/go_router/test/go_router_test.dart @@ -61,7 +61,7 @@ void main() { final GoRouter router = await createRouter(routes, tester); router.go('/'); - final List matches = + final List matches = router.routerDelegate.currentConfiguration.matches; expect(matches, hasLength(1)); expect((matches.first.route as GoRoute).name, '1'); @@ -135,7 +135,7 @@ void main() { ); router.go('/foo'); await tester.pumpAndSettle(); - final List matches = + final List matches = router.routerDelegate.currentConfiguration.matches; expect(matches, hasLength(0)); expect(find.byType(TestErrorScreen), findsOneWidget); @@ -156,7 +156,7 @@ void main() { final GoRouter router = await createRouter(routes, tester); router.go('/login'); await tester.pumpAndSettle(); - final List matches = + final List matches = router.routerDelegate.currentConfiguration.matches; expect(matches, hasLength(1)); expect(matches.first.matchedLocation, '/login'); @@ -186,7 +186,7 @@ void main() { final GoRouter router = await createRouter(routes, tester); router.go('/login'); await tester.pumpAndSettle(); - final List matches = + final List matches = router.routerDelegate.currentConfiguration.matches; expect(matches, hasLength(1)); expect(matches.first.matchedLocation, '/login'); @@ -211,7 +211,7 @@ void main() { final GoRouter router = await createRouter(routes, tester); router.go('/login/'); await tester.pumpAndSettle(); - final List matches = + final List matches = router.routerDelegate.currentConfiguration.matches; expect(matches, hasLength(1)); expect(matches.first.matchedLocation, '/login'); @@ -231,7 +231,7 @@ void main() { final GoRouter router = await createRouter(routes, tester); router.go('/profile/'); await tester.pumpAndSettle(); - final List matches = + final List matches = router.routerDelegate.currentConfiguration.matches; expect(matches, hasLength(1)); expect(matches.first.matchedLocation, '/profile/foo'); @@ -251,7 +251,7 @@ void main() { final GoRouter router = await createRouter(routes, tester); router.go('/profile/?bar=baz'); await tester.pumpAndSettle(); - final List matches = + final List matches = router.routerDelegate.currentConfiguration.matches; expect(matches, hasLength(1)); expect(matches.first.matchedLocation, '/profile/foo'); @@ -348,7 +348,7 @@ void main() { router.pop(); await tester.pumpAndSettle(); - final List matches = + final List matches = router.routerDelegate.currentConfiguration.matches; expect(matches.length, 4); expect(find.byType(HomeScreen), findsNothing); @@ -375,7 +375,7 @@ void main() { final GoRouter router = await createRouter(routes, tester); router.go('/login'); await tester.pumpAndSettle(); - final List matches = + final List matches = router.routerDelegate.currentConfiguration.matches; expect(matches.length, 2); expect(matches.first.matchedLocation, '/'); @@ -497,7 +497,7 @@ void main() { final GoRouter router = await createRouter(routes, tester); router.go('/bar'); await tester.pumpAndSettle(); - List matches = + List matches = router.routerDelegate.currentConfiguration.matches; expect(matches, hasLength(2)); expect(find.byType(Page1Screen), findsOneWidget); @@ -621,7 +621,7 @@ void main() { const String loc = '/FaMiLy/f2'; router.go(loc); await tester.pumpAndSettle(); - final List matches = + final List matches = router.routerDelegate.currentConfiguration.matches; // NOTE: match the lower case, since location is canonicalized to match the @@ -650,7 +650,7 @@ void main() { final GoRouter router = await createRouter(routes, tester); router.go('/user'); await tester.pumpAndSettle(); - final List matches = + final List matches = router.routerDelegate.currentConfiguration.matches; expect(matches, hasLength(1)); expect(find.byType(DummyScreen), findsOneWidget); @@ -1927,7 +1927,7 @@ void main() { errorBuilder: (BuildContext context, GoRouterState state) => TestErrorScreen(state.error!), ); - final List matches = + final List matches = router.routerDelegate.currentConfiguration.matches; expect(matches, hasLength(0)); expect(find.byType(TestErrorScreen), findsOneWidget); @@ -1955,7 +1955,7 @@ void main() { TestErrorScreen(state.error!), ); - final List matches = + final List matches = router.routerDelegate.currentConfiguration.matches; expect(matches, hasLength(0)); expect(find.byType(TestErrorScreen), findsOneWidget); @@ -1980,7 +1980,7 @@ void main() { TestErrorScreen(state.error!), ); - final List matches = + final List matches = router.routerDelegate.currentConfiguration.matches; expect(matches, hasLength(0)); expect(find.byType(TestErrorScreen), findsOneWidget); @@ -2004,7 +2004,7 @@ void main() { TestErrorScreen(state.error!), ); - final List matches = + final List matches = router.routerDelegate.currentConfiguration.matches; expect(matches, hasLength(0)); expect(find.byType(TestErrorScreen), findsOneWidget); @@ -2066,7 +2066,7 @@ void main() { }, ); - final List matches = + final List matches = router.routerDelegate.currentConfiguration.matches; expect(matches, hasLength(1)); expect(find.byType(LoginScreen), findsOneWidget); @@ -2101,7 +2101,7 @@ void main() { }, ); - final List matches = + final List matches = router.routerDelegate.currentConfiguration.matches; expect(matches, hasLength(2)); }); @@ -2130,7 +2130,7 @@ void main() { initialLocation: loc, ); - final List matches = + final List matches = router.routerDelegate.currentConfiguration.matches; expect(matches, hasLength(1)); expect(find.byType(HomeScreen), findsOneWidget); @@ -2172,7 +2172,7 @@ void main() { initialLocation: '/family/f2/person/p1', ); - final List matches = + final List matches = router.routerDelegate.currentConfiguration.matches; expect(matches.length, 3); expect(find.byType(HomeScreen, skipOffstage: false), findsOneWidget); @@ -2194,7 +2194,7 @@ void main() { redirectLimit: 10, ); - final List matches = + final List matches = router.routerDelegate.currentConfiguration.matches; expect(matches, hasLength(0)); expect(find.byType(TestErrorScreen), findsOneWidget); @@ -2981,7 +2981,7 @@ void main() { 'q2': ['v2', 'v3'], }); await tester.pumpAndSettle(); - final List matches = + final List matches = router.routerDelegate.currentConfiguration.matches; expect(matches, hasLength(1)); @@ -3033,7 +3033,7 @@ void main() { router.go('/page?q1=v1&q2=v2&q2=v3'); await tester.pumpAndSettle(); - final List matches = + final List matches = router.routerDelegate.currentConfiguration.matches; expect(matches, hasLength(1)); @@ -3084,7 +3084,7 @@ void main() { router.go('/page?q1=v1&q2=v2&q2=v3'); await tester.pumpAndSettle(); - final List matches = + final List matches = router.routerDelegate.currentConfiguration.matches; expect(matches, hasLength(1)); diff --git a/packages/go_router/test/test_helpers.dart b/packages/go_router/test/test_helpers.dart index 3db0b4579086..112617832001 100644 --- a/packages/go_router/test/test_helpers.dart +++ b/packages/go_router/test/test_helpers.dart @@ -356,14 +356,6 @@ StatefulShellRouteBuilder mockStackedShellBuilder = (BuildContext context, return navigationShell; }; -RouteMatch createRouteMatch(RouteBase route, String location) { - return RouteMatch( - route: route, - matchedLocation: location, - pageKey: ValueKey(location), - ); -} - /// A routing config that is never going to change. class ConstantRoutingConfig extends ValueListenable { const ConstantRoutingConfig(this.value);