From 370a2795ab8e99274f0fa3b8be8d8299178ab05f Mon Sep 17 00:00:00 2001 From: John Ryan Date: Fri, 1 Jul 2022 12:30:22 -0700 Subject: [PATCH 01/35] Refactor internal classes and methods - Separate matching from redirection - Add RouteRedirector typedef - Add RouteMatcher class - Add RouteBuilder class - Add RouteConfiguration class - Rename and reorganize internal classes and libraries - Add todo comments --- packages/go_router/.gitignore | 30 ++ .../example/lib/router_stream_refresh.dart | 3 +- packages/go_router/lib/go_router.dart | 312 +++++++++++- .../{go_router_delegate.dart => builder.dart} | 137 ++---- packages/go_router/lib/src/configuration.dart | 149 ++++++ packages/go_router/lib/src/delegate.dart | 95 ++++ .../lib/src/go_route_information_parser.dart | 444 ------------------ packages/go_router/lib/src/go_router.dart | 234 --------- ...rovider.dart => information_provider.dart} | 7 +- .../lib/src/inherited_go_router.dart | 38 -- .../src/{go_route_match.dart => match.dart} | 16 +- packages/go_router/lib/src/matching.dart | 190 ++++++++ .../error_screen.dart} | 8 +- .../refresh_stream.dart} | 12 +- .../cupertino.dart} | 6 +- .../{ => pages}/custom_transition_page.dart | 0 .../material.dart} | 6 +- packages/go_router/lib/src/parser.dart | 117 +++++ .../src/{path_parser.dart => path_utils.dart} | 38 ++ packages/go_router/lib/src/platform.dart | 8 + .../{ => platform}/path_strategy_nonweb.dart | 1 + .../src/{ => platform}/path_strategy_web.dart | 0 .../src/{ => platform}/url_path_strategy.dart | 0 packages/go_router/lib/src/redirection.dart | 156 ++++++ .../lib/src/{go_route.dart => route.dart} | 8 +- packages/go_router/lib/src/routing.dart | 7 + .../src/{go_router_state.dart => state.dart} | 11 +- .../{route_data.dart => typed_routing.dart} | 4 +- packages/go_router/lib/src/typedefs.dart | 55 +-- packages/go_router/pubspec.yaml | 2 +- ...upertino_test.dart => cupertino_test.dart} | 10 +- ..._delegate_test.dart => delegate_test.dart} | 18 +- ...or_page_test.dart => error_page_test.dart} | 10 +- packages/go_router/test/go_router_test.dart | 173 +++---- .../{ => helpers}/error_screen_helpers.dart | 2 +- ...st.dart => information_provider_test.dart} | 2 +- ...o_router_test.dart => inherited_test.dart} | 0 ..._material_test.dart => material_test.dart} | 10 +- ...tion_parser_test.dart => parser_test.dart} | 122 +++-- packages/go_router/test/path_parser_test.dart | 2 +- 40 files changed, 1378 insertions(+), 1065 deletions(-) create mode 100644 packages/go_router/.gitignore rename packages/go_router/lib/src/{go_router_delegate.dart => builder.dart} (70%) create mode 100644 packages/go_router/lib/src/configuration.dart create mode 100644 packages/go_router/lib/src/delegate.dart delete mode 100644 packages/go_router/lib/src/go_route_information_parser.dart delete mode 100644 packages/go_router/lib/src/go_router.dart rename packages/go_router/lib/src/{go_route_information_provider.dart => information_provider.dart} (94%) delete mode 100644 packages/go_router/lib/src/inherited_go_router.dart rename packages/go_router/lib/src/{go_route_match.dart => match.dart} (92%) create mode 100644 packages/go_router/lib/src/matching.dart rename packages/go_router/lib/src/{go_router_error_page.dart => misc/error_screen.dart} (90%) rename packages/go_router/lib/src/{go_router_refresh_stream.dart => misc/refresh_stream.dart} (69%) rename packages/go_router/lib/src/{go_router_cupertino.dart => pages/cupertino.dart} (90%) rename packages/go_router/lib/src/{ => pages}/custom_transition_page.dart (100%) rename packages/go_router/lib/src/{go_router_material.dart => pages/material.dart} (89%) create mode 100644 packages/go_router/lib/src/parser.dart rename packages/go_router/lib/src/{path_parser.dart => path_utils.dart} (74%) create mode 100644 packages/go_router/lib/src/platform.dart rename packages/go_router/lib/src/{ => platform}/path_strategy_nonweb.dart (90%) rename packages/go_router/lib/src/{ => platform}/path_strategy_web.dart (100%) rename packages/go_router/lib/src/{ => platform}/url_path_strategy.dart (100%) create mode 100644 packages/go_router/lib/src/redirection.dart rename packages/go_router/lib/src/{go_route.dart => route.dart} (97%) create mode 100644 packages/go_router/lib/src/routing.dart rename packages/go_router/lib/src/{go_router_state.dart => state.dart} (88%) rename packages/go_router/lib/src/{route_data.dart => typed_routing.dart} (98%) rename packages/go_router/test/{go_router_cupertino_test.dart => cupertino_test.dart} (92%) rename packages/go_router/test/{go_router_delegate_test.dart => delegate_test.dart} (85%) rename packages/go_router/test/{go_router_error_page_test.dart => error_page_test.dart} (86%) rename packages/go_router/test/{ => helpers}/error_screen_helpers.dart (98%) rename packages/go_router/test/{go_route_information_provider_test.dart => information_provider_test.dart} (94%) rename packages/go_router/test/{inherited_go_router_test.dart => inherited_test.dart} (100%) rename packages/go_router/test/{go_router_material_test.dart => material_test.dart} (92%) rename packages/go_router/test/{go_route_information_parser_test.dart => parser_test.dart} (67%) diff --git a/packages/go_router/.gitignore b/packages/go_router/.gitignore new file mode 100644 index 00000000000..96486fd9302 --- /dev/null +++ b/packages/go_router/.gitignore @@ -0,0 +1,30 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ +migrate_working_dir/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +# Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock. +/pubspec.lock +**/doc/api/ +.dart_tool/ +.packages +build/ diff --git a/packages/go_router/example/lib/router_stream_refresh.dart b/packages/go_router/example/lib/router_stream_refresh.dart index 2d00dbe4ef7..baa3d767417 100644 --- a/packages/go_router/example/lib/router_stream_refresh.dart +++ b/packages/go_router/example/lib/router_stream_refresh.dart @@ -84,7 +84,8 @@ class _AppState extends State { return null; }, // changes on the listenable will cause the router to refresh it's route - refreshListenable: GoRouterRefreshStream(loggedInState.stream), + // TODO(johnpryan): Change type to Stream, remove GoRouterRefreshStream + refreshListenable: StreamListenable(loggedInState.stream), ); super.initState(); } diff --git a/packages/go_router/lib/go_router.dart b/packages/go_router/lib/go_router.dart index 56bd9579d67..2d52837fb30 100644 --- a/packages/go_router/lib/go_router.dart +++ b/packages/go_router/lib/go_router.dart @@ -6,31 +6,291 @@ /// deep linking, data-driven routes and more library go_router; +import 'package:flutter/rendering.dart'; import 'package:flutter/widgets.dart'; -import 'src/go_router.dart'; +import 'src/configuration.dart'; +import 'src/logging.dart'; +import 'src/matching.dart'; +import 'src/platform.dart'; +import 'src/routing.dart'; +import 'src/typedefs.dart'; -export 'src/custom_transition_page.dart'; -export 'src/go_route.dart'; -export 'src/go_router.dart'; -export 'src/go_router_refresh_stream.dart'; -export 'src/go_router_state.dart'; -export 'src/inherited_go_router.dart'; -export 'src/route_data.dart' show GoRouteData, TypedGoRoute; -export 'src/typedefs.dart' show GoRouterPageBuilder, GoRouterRedirect; -export 'src/url_path_strategy.dart'; +export 'src/configuration.dart' show GoRouterState, GoRoute; +export 'src/misc/refresh_stream.dart'; +export 'src/pages/custom_transition_page.dart'; +export 'src/platform.dart' show UrlPathStrategy; +export 'src/typed_routing.dart' show GoRouteData, TypedGoRoute; +export 'src/typedefs.dart'; + +/// The top-level go router class. +/// +/// Create one of these to initialize your app's routing policy. +// ignore: prefer_mixin +class GoRouter extends ChangeNotifier with NavigatorObserver { + /// Default constructor to configure a GoRouter with a routes builder + /// and an error page builder. + GoRouter({ + required List routes, + // TODO(johnpryan): Change to a route, improve error API + GoRouterPageBuilder? errorPageBuilder, + GoRouterWidgetBuilder? errorBuilder, + GoRouterRedirect? redirect, + Listenable? refreshListenable, + int redirectLimit = 5, + bool routerNeglect = false, + String? initialLocation, + // TODO(johnpryan): Deprecate this parameter + UrlPathStrategy? urlPathStrategy, + List? observers, + bool debugLogDiagnostics = false, + // TODO(johnpryan): Deprecate this parameter + GoRouterNavigatorBuilder? navigatorBuilder, + String? restorationScopeId, + }) { + if (urlPathStrategy != null) { + setUrlPathStrategy(urlPathStrategy); + } + + setLogging(enabled: debugLogDiagnostics); + WidgetsFlutterBinding.ensureInitialized(); + + routeConfiguration = RouteConfiguration( + routes: routes, + topRedirect: redirect ?? (_) => null, + redirectLimit: redirectLimit, + ); + + routeConfiguration.validate(); + + routeInformationParser = GoRouterInformationParser( + configuration: routeConfiguration, + debugRequireGoRouteInformationProvider: true, + ); + routeInformationProvider = GoRouteInformationProvider( + initialRouteInformation: RouteInformation( + location: _effectiveInitialLocation(initialLocation)), + refreshListenable: refreshListenable); + + routerDelegate = GoRouterDelegate( + configuration: routeConfiguration, + errorPageBuilder: errorPageBuilder, + errorBuilder: errorBuilder, + routerNeglect: routerNeglect, + observers: [ + ...observers ?? [], + this + ], + restorationScopeId: restorationScopeId, + // wrap the returned Navigator to enable GoRouter.of(context).go() et al, + // allowing the caller to wrap the navigator themselves + builderWithNav: + (BuildContext context, GoRouterState state, Navigator nav) => + InheritedGoRouter( + goRouter: this, + child: navigatorBuilder?.call(context, state, nav) ?? nav, + ), + ); + assert(() { + log.info('setting initial location $initialLocation'); + return true; + }()); + } + + /// The route configuration for the app. + late final RouteConfiguration routeConfiguration; + + /// The route information parser used by the go router. + late final GoRouterInformationParser routeInformationParser; + + /// The router delegate used by the go router. + late final GoRouterDelegate routerDelegate; + + /// The route information provider used by the go router. + late final GoRouteInformationProvider routeInformationProvider; + + /// Get the current location. + String get location => + routerDelegate.currentConfiguration.location.toString(); + + /// Get a location from route name and parameters. + /// This is useful for redirecting to a named location. + String namedLocation( + String name, { + Map params = const {}, + Map queryParams = const {}, + }) => + routeInformationParser.configuration.namedLocation( + name, + params: params, + queryParams: queryParams, + ); + + /// Navigate to a URI location w/ optional query parameters, e.g. + /// `/family/f2/person/p1?color=blue` + void go(String location, {Object? extra}) { + assert(() { + log.info('going to $location'); + return true; + }()); + routeInformationProvider.value = + RouteInformation(location: location, state: extra); + } + + /// Navigate to a named route w/ optional parameters, e.g. + /// `name='person', params={'fid': 'f2', 'pid': 'p1'}` + /// Navigate to the named route. + void goNamed( + String name, { + Map params = const {}, + Map queryParams = const {}, + Object? extra, + }) => + go( + namedLocation(name, params: params, queryParams: queryParams), + extra: extra, + ); + + /// 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}) { + assert(() { + log.info('pushing $location'); + return true; + }()); + routeInformationParser + .parseRouteInformation( + DebugGoRouteInformation(location: location, state: extra)) + .then((RouteMatchList matches) { + routerDelegate.push(matches.last); + }); + } + + /// Push a named route onto the page stack w/ optional parameters, e.g. + /// `name='person', params={'fid': 'f2', 'pid': 'p1'}` + void pushNamed( + String name, { + Map params = const {}, + Map queryParams = const {}, + Object? extra, + }) => + push( + namedLocation(name, params: params, queryParams: queryParams), + extra: extra, + ); + + /// Returns `true` if there is more than 1 page on the stack. + bool canPop() => routerDelegate.canPop(); + + /// Pop the top page off the GoRouter's page stack. + void pop() { + assert(() { + log.info('popping $location'); + return true; + }()); + routerDelegate.pop(); + } + + /// Refresh the route. + void refresh() { + assert(() { + log.info('refreshing $location'); + return true; + }()); + routeInformationProvider.notifyListeners(); + } + + /// Set the app's URL path strategy (defaults to hash). call before runApp(). + static void setUrlPathStrategy(UrlPathStrategy strategy) => + setUrlPathStrategyImpl(strategy); + + /// Find the current GoRouter in the widget tree. + static GoRouter of(BuildContext context) { + final InheritedGoRouter? inherited = + context.dependOnInheritedWidgetOfExactType(); + assert(inherited != null, 'No GoRouter found in context'); + return inherited!.goRouter; + } + + /// The [Navigator] pushed `route`. + @override + void didPush(Route route, Route? previousRoute) => + notifyListeners(); + + /// The [Navigator] popped `route`. + @override + void didPop(Route route, Route? previousRoute) => + notifyListeners(); + + /// The [Navigator] removed `route`. + @override + void didRemove(Route route, Route? previousRoute) => + notifyListeners(); + + /// The [Navigator] replaced `oldRoute` with `newRoute`. + @override + void didReplace({Route? newRoute, Route? oldRoute}) => + notifyListeners(); + + @override + void dispose() { + routeInformationProvider.dispose(); + routerDelegate.dispose(); + super.dispose(); + } + + String _effectiveInitialLocation(String? initialLocation) { + final String platformDefault = + WidgetsBinding.instance.platformDispatcher.defaultRouteName; + if (initialLocation == null) { + return platformDefault; + } else if (platformDefault == '/') { + return initialLocation; + } else { + return platformDefault; + } + } +} + +/// GoRouter implementation of InheritedWidget. +/// +/// Used for to find the current GoRouter in the widget tree. This is useful +/// when routing from anywhere in your app. +class InheritedGoRouter extends InheritedWidget { + /// Default constructor for the inherited go router. + const InheritedGoRouter({ + required Widget child, + required this.goRouter, + Key? key, + }) : super(child: child, key: key); + + /// The [GoRouter] that is made available to the widget tree. + final GoRouter goRouter; + + /// Used by the Router architecture as part of the InheritedWidget. + @override + // ignore: prefer_expression_function_bodies + bool updateShouldNotify(covariant InheritedGoRouter oldWidget) { + // avoid rebuilding the widget tree if the router has not changed + return goRouter != oldWidget.goRouter; + } + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties.add(DiagnosticsProperty('goRouter', goRouter)); + } +} /// Dart extension to add navigation function to a BuildContext object, e.g. /// context.go('/'); -// NOTE: adding this here instead of in /src to work-around a Dart analyzer bug -// and fix: https://github.com/csells/go_router/issues/116 extension GoRouterHelper on BuildContext { /// Get a location from route name and parameters. String namedLocation( - String name, { - Map params = const {}, - Map queryParams = const {}, - }) => + String name, { + Map params = const {}, + Map queryParams = const {}, + }) => GoRouter.of(this) .namedLocation(name, params: params, queryParams: queryParams); @@ -40,11 +300,11 @@ extension GoRouterHelper on BuildContext { /// Navigate to a named route. void goNamed( - String name, { - Map params = const {}, - Map queryParams = const {}, - Object? extra, - }) => + String name, { + Map params = const {}, + Map queryParams = const {}, + Object? extra, + }) => GoRouter.of(this).goNamed( name, params: params, @@ -58,11 +318,11 @@ extension GoRouterHelper on BuildContext { /// Navigate to a named route onto the page stack. void pushNamed( - String name, { - Map params = const {}, - Map queryParams = const {}, - Object? extra, - }) => + String name, { + Map params = const {}, + Map queryParams = const {}, + Object? extra, + }) => GoRouter.of(this).pushNamed( name, params: params, diff --git a/packages/go_router/lib/src/go_router_delegate.dart b/packages/go_router/lib/src/builder.dart similarity index 70% rename from packages/go_router/lib/src/go_router_delegate.dart rename to packages/go_router/lib/src/builder.dart index 3ea7e624ea7..eb37dcad7e1 100644 --- a/packages/go_router/lib/src/go_router_delegate.dart +++ b/packages/go_router/lib/src/builder.dart @@ -2,40 +2,31 @@ // 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:flutter/foundation.dart'; import 'package:flutter/widgets.dart'; -import 'custom_transition_page.dart'; -import 'go_route_information_parser.dart'; -import 'go_route_match.dart'; -import 'go_router_cupertino.dart'; -import 'go_router_error_page.dart'; -import 'go_router_material.dart'; -import 'go_router_state.dart'; +import 'configuration.dart'; import 'logging.dart'; -import 'route_data.dart'; +import 'match.dart'; +import 'matching.dart'; +import 'misc/error_screen.dart'; +import 'pages/cupertino.dart'; +import 'pages/custom_transition_page.dart'; +import 'pages/material.dart'; +import 'typed_routing.dart'; import 'typedefs.dart'; -/// GoRouter implementation of the RouterDelegate base class. -class GoRouterDelegate extends RouterDelegate> - with PopNavigatorRouterDelegateMixin>, ChangeNotifier { - /// Constructor for GoRouter's implementation of the - /// RouterDelegate base class. - GoRouterDelegate( - this._parser, { +/// Builds the top-level Navigator for GoRouter +class RouteBuilder { + /// GoRouteBuilder constructor + RouteBuilder({ + required this.configuration, required this.builderWithNav, required this.errorPageBuilder, required this.errorBuilder, + required this.restorationScopeId, required this.observers, - required this.routerNeglect, - this.restorationScopeId, }); - // TODO(chunhtai): remove this once namedLocation is removed from go_router. - final GoRouteInformationParser _parser; - /// Builder function for a go router with Navigator. final GoRouterBuilderWithNav builderWithNav; @@ -45,77 +36,39 @@ class GoRouterDelegate extends RouterDelegate> /// Error widget builder for the go router delegate. final GoRouterWidgetBuilder? errorBuilder; - /// NavigatorObserver used to receive change notifications when - /// navigation changes. - final List observers; - - /// Set to true to disable creating history entries on the web. - final bool routerNeglect; + /// The route configuration for the app. + final RouteConfiguration configuration; /// Restoration ID to save and restore the state of the navigator, including /// its history. final String? restorationScopeId; - final GlobalKey _key = GlobalKey(); - List _matches = const []; - - /// Push the given location onto the page stack - void push(GoRouteMatch match) { - _matches.add(match); - notifyListeners(); - } - - /// Returns `true` if there is more than 1 page on the stack. - bool canPop() { - return _matches.length > 1; - } - - /// Pop the top page off the GoRouter's page stack. - void pop() { - _matches.remove(_matches.last); - assert(_matches.isNotEmpty, - 'have popped the last page off of the stack; there are no pages left to show'); - notifyListeners(); - } - - /// For internal use; visible for testing only. - @visibleForTesting - List get matches => _matches; - - /// For use by the Router architecture as part of the RouterDelegate. - @override - GlobalKey get navigatorKey => _key; - - /// For use by the Router architecture as part of the RouterDelegate. - @override - List get currentConfiguration => _matches; - - /// For use by the Router architecture as part of the RouterDelegate. - @override - Widget build(BuildContext context) => _builder(context, _matches); - - /// For use by the Router architecture as part of the RouterDelegate. - @override - Future setNewRoutePath(List configuration) { - _matches = configuration; - // Use [SynchronousFuture] so that the initial url is processed - // synchronously and remove unwanted initial animations on deep-linking - return SynchronousFuture(null); - } + /// NavigatorObserver used to receive change notifications when + /// navigation changes. + final List observers; - Widget _builder(BuildContext context, Iterable matches) { + /// Builds the top-level Navigator by invoking the build method on each + /// matching route + Widget build( + BuildContext context, + RouteMatchList matches, + VoidCallback pop, + Key navigatorKey, + bool routerNeglect, + ) { List>? pages; Exception? error; - final String location = matches.last.fullUriString; + final String location = matches.location.toString(); + final List matchesList = matches.matches; try { // build the stack of pages if (routerNeglect) { Router.neglect( context, - () => pages = getPages(context, matches.toList()).toList(), + () => pages = getPages(context, matchesList).toList(), ); } else { - pages = getPages(context, matches.toList()).toList(); + pages = getPages(context, matchesList).toList(); } // note that we need to catch it this way to get all the info, e.g. the @@ -135,7 +88,7 @@ class GoRouterDelegate extends RouterDelegate> _errorPageBuilder( context, GoRouterState( - _parser, + configuration, location: location, subloc: uri.path, name: null, @@ -151,8 +104,8 @@ class GoRouterDelegate extends RouterDelegate> // pass either the match error or the build error along to the navigator // builder, preferring the match error - if (matches.length == 1 && matches.first.error != null) { - error = matches.first.error; + if (matches.matches.length == 1 && matches.matches.first.error != null) { + error = matches.matches.first.error; } // wrap the returned Navigator to enable GoRouter.of(context).go() @@ -160,9 +113,10 @@ class GoRouterDelegate extends RouterDelegate> return builderWithNav( context, GoRouterState( - _parser, + configuration, location: location, - name: null, // no name available at the top level + name: null, + // no name available at the top level // trim the query params off the subloc to match route.redirect subloc: uri.path, // pass along the query params 'cuz that's all we have right now @@ -172,7 +126,8 @@ class GoRouterDelegate extends RouterDelegate> ), Navigator( restorationScopeId: restorationScopeId, - key: _key, // needed to enable Android system Back button + key: navigatorKey, + // needed to enable Android system Back button pages: pages!, observers: observers, onPopPage: (Route route, dynamic result) { @@ -209,19 +164,19 @@ class GoRouterDelegate extends RouterDelegate> @visibleForTesting Iterable> getPages( BuildContext context, - List matches, + List matches, ) sync* { assert(matches.isNotEmpty); Map params = {}; - for (final GoRouteMatch match in matches) { + for (final RouteMatch match in matches) { // merge new params to keep params from previously matched paths, e.g. // /family/:fid/person/:pid provides fid and pid to person/:pid params = {...params, ...match.decodedParams}; // get a page from the builder and associate it with a sub-location final GoRouterState state = GoRouterState( - _parser, + configuration, location: match.fullUriString, subloc: match.subloc, name: match.route.name, @@ -279,7 +234,7 @@ class GoRouterDelegate extends RouterDelegate> }()); _pageBuilderForAppType = pageBuilderForMaterialApp; _errorBuilderForAppType = (BuildContext c, GoRouterState s) => - GoRouterMaterialErrorScreen(s.error); + MaterialErrorScreen(s.error); } else if (elem != null && isCupertinoApp(elem)) { assert(() { log.info('CupertinoApp found'); @@ -287,7 +242,7 @@ class GoRouterDelegate extends RouterDelegate> }()); _pageBuilderForAppType = pageBuilderForCupertinoApp; _errorBuilderForAppType = (BuildContext c, GoRouterState s) => - GoRouterCupertinoErrorScreen(s.error); + CupertinoErrorScreen(s.error); } else { assert(() { log.info('WidgetsApp found'); @@ -295,7 +250,7 @@ class GoRouterDelegate extends RouterDelegate> }()); _pageBuilderForAppType = pageBuilderForWidgetApp; _errorBuilderForAppType = - (BuildContext c, GoRouterState s) => GoRouterErrorScreen(s.error); + (BuildContext c, GoRouterState s) => ErrorScreen(s.error); } } diff --git a/packages/go_router/lib/src/configuration.dart b/packages/go_router/lib/src/configuration.dart new file mode 100644 index 00000000000..399a7aafcb4 --- /dev/null +++ b/packages/go_router/lib/src/configuration.dart @@ -0,0 +1,149 @@ +// 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 'configuration.dart'; +import 'logging.dart'; +import 'path_utils.dart'; +import 'typedefs.dart'; + +export 'route.dart'; +export 'state.dart'; + +/// The route configuration for GoRouter, configured by the app +class RouteConfiguration { + /// Constructs a GoRouterConfiguration + RouteConfiguration({ + required this.routes, + required this.redirectLimit, + required this.topRedirect, + }) { + _cacheNameToPath('', routes); + assert(() { + _debugKnownRoutes(); + return true; + }()); + } + + /// List of top level routes used by the go router delegate + final List routes; + + /// The limit for the number of consecutive redirects + final int redirectLimit; + + /// Top level page redirect. + final GoRouterRedirect topRedirect; + + final Map _nameToPath = {}; + + /// Throws a [RouteConfigurationError] if this configuration is invalid + void validate() { + for (final GoRoute route in routes) { + if (!route.path.startsWith('/')) { + throw RouteConfigurationError( + 'top-level path must start with "/": ${route.path}'); + } + } + } + + /// Looks up the url location by a [GoRoute]'s name + String namedLocation( + String name, { + Map params = const {}, + Map queryParams = const {}, + }) { + assert(() { + log.info('getting location for name: ' + '"$name"' + '${params.isEmpty ? '' : ', params: $params'}' + '${queryParams.isEmpty ? '' : ', queryParams: $queryParams'}'); + return true; + }()); + assert(_nameToPath.containsKey(name), 'unknown route name: $name'); + final String path = _nameToPath[name]!; + assert(() { + // Check that all required params are present + final List paramNames = []; + patternToRegExp(path, paramNames); + for (final String paramName in paramNames) { + assert(params.containsKey(paramName), + 'missing param "$paramName" for $path'); + } + + // Check that there are no extra params + for (final String key in params.keys) { + assert(paramNames.contains(key), 'unknown param "$key" for $path'); + } + return true; + }()); + final Map encodedParams = { + for (final MapEntry param in params.entries) + param.key: Uri.encodeComponent(param.value) + }; + final String location = patternToPath(path, encodedParams); + return Uri(path: location, queryParameters: queryParams).toString(); + } + + @override + String toString() { + return 'GoRouterConfiguration: $routes'; + } + + String _debugKnownRoutes() { + final StringBuffer sb = StringBuffer(); + sb.writeln('Full paths for routes:'); + _debugFullPathsFor(routes, '', 0, sb); + + if (_nameToPath.isNotEmpty) { + sb.writeln('known full paths for route names:'); + for (final MapEntry e in _nameToPath.entries) { + sb.writeln(' ${e.key} => ${e.value}'); + } + } + + return sb.toString(); + } + + void _debugFullPathsFor( + List routes, String parentFullpath, int depth, StringBuffer sb) { + for (final GoRoute route in routes) { + final String fullpath = concatenatePaths(parentFullpath, route.path); + assert(() { + sb.writeln(' => ${''.padLeft(depth * 2)}$fullpath'); + return true; + }()); + _debugFullPathsFor(route.routes, fullpath, depth + 1, sb); + } + } + + void _cacheNameToPath(String parentFullPath, List childRoutes) { + for (final GoRoute route in childRoutes) { + final String fullPath = concatenatePaths(parentFullPath, route.path); + + if (route.name != null) { + final String name = route.name!.toLowerCase(); + assert( + !_nameToPath.containsKey(name), + 'duplication fullpaths for name ' + '"$name":${_nameToPath[name]}, $fullPath'); + _nameToPath[name] = fullPath; + } + + if (route.routes.isNotEmpty) { + _cacheNameToPath(fullPath, route.routes); + } + } + } +} + +/// Thrown when the [RouteConfiguration] is invalid +class RouteConfigurationError extends Error { + /// RouteConfigurationError constructor + RouteConfigurationError(this.message); + + /// The error message + final String message; + + @override + String toString() => 'Route configuration error: $message'; +} diff --git a/packages/go_router/lib/src/delegate.dart b/packages/go_router/lib/src/delegate.dart new file mode 100644 index 00000000000..463c9d22ebb --- /dev/null +++ b/packages/go_router/lib/src/delegate.dart @@ -0,0 +1,95 @@ +// 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 'dart:async'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/widgets.dart'; + +import 'builder.dart'; +import 'configuration.dart'; +import 'match.dart'; +import 'matching.dart'; +import 'typedefs.dart'; + +/// GoRouter implementation of the RouterDelegate base class. +class GoRouterDelegate extends RouterDelegate + with PopNavigatorRouterDelegateMixin, ChangeNotifier { + /// Constructor for GoRouter's implementation of the + /// RouterDelegate base class. + GoRouterDelegate({ + required RouteConfiguration configuration, + required GoRouterBuilderWithNav builderWithNav, + required GoRouterPageBuilder? errorPageBuilder, + required GoRouterWidgetBuilder? errorBuilder, + required List observers, + required this.routerNeglect, + String? restorationScopeId, + }) : builder = RouteBuilder( + configuration: configuration, + builderWithNav: builderWithNav, + errorPageBuilder: errorPageBuilder, + errorBuilder: errorBuilder, + restorationScopeId: restorationScopeId, + observers: observers, + ); + + /// Builds the top-level Navigator given a configuration and location. + final RouteBuilder builder; + + /// Set to true to disable creating history entries on the web. + final bool routerNeglect; + + final GlobalKey _key = GlobalKey(); + + RouteMatchList _matches = RouteMatchList.empty(); + + /// Push the given location onto the page stack + void push(RouteMatch match) { + _matches.push(match); + notifyListeners(); + } + + /// Returns `true` if there is more than 1 page on the stack. + bool canPop() { + return _matches.canPop(); + } + + /// Pop the top page off the GoRouter's page stack. + void pop() { + _matches.pop(); + notifyListeners(); + } + + /// For internal use; visible for testing only. + @visibleForTesting + RouteMatchList get matches => _matches; + + /// For use by the Router architecture as part of the RouterDelegate. + @override + GlobalKey get navigatorKey => _key; + + /// For use by the Router architecture as part of the RouterDelegate. + @override + RouteMatchList get currentConfiguration => _matches; + + /// For use by the Router architecture as part of the RouterDelegate. + @override + Widget build(BuildContext context) => builder.build( + context, + _matches, + pop, + navigatorKey, + routerNeglect, + ); + + /// For use by the Router architecture as part of the RouterDelegate. + @override + Future setNewRoutePath(RouteMatchList configuration) { + _matches = configuration; + // Use [SynchronousFuture] so that the initial url is processed + // synchronously and remove unwanted initial animations on deep-linking + return SynchronousFuture(null); + } +} diff --git a/packages/go_router/lib/src/go_route_information_parser.dart b/packages/go_router/lib/src/go_route_information_parser.dart deleted file mode 100644 index 4fc114999b7..00000000000 --- a/packages/go_router/lib/src/go_route_information_parser.dart +++ /dev/null @@ -1,444 +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/foundation.dart'; -import 'package:flutter/widgets.dart'; -import 'package:go_router/src/go_route_information_provider.dart'; - -import 'go_route.dart'; -import 'go_route_match.dart'; -import 'go_router_state.dart'; -import 'logging.dart'; -import 'path_parser.dart'; -import 'typedefs.dart'; - -class _ParserError extends Error implements UnsupportedError { - _ParserError(this.message); - - @override - final String? message; -} - -/// GoRouter implementation of the RouteInformationParser base class -class GoRouteInformationParser - extends RouteInformationParser> { - /// Creates a [GoRouteInformationParser]. - GoRouteInformationParser({ - required this.routes, - required this.redirectLimit, - required this.topRedirect, - this.debugRequireGoRouteInformationProvider = false, - }) : assert(() { - // check top-level route paths are valid - for (final GoRoute route in routes) { - assert(route.path.startsWith('/'), - 'top-level path must start with "/": ${route.path}'); - } - return true; - }()) { - _cacheNameToPath('', routes); - assert(() { - _debugLogKnownRoutes(); - return true; - }()); - } - - /// List of top level routes used by the go router delegate. - final List routes; - - /// The limit for the number of consecutive redirects. - final int redirectLimit; - - /// Top level page redirect. - final GoRouterRedirect topRedirect; - - /// A debug property to assert [GoRouteInformationProvider] is in use along - /// with this parser. - /// - /// An assertion error will be thrown if this property set to true and the - /// [GoRouteInformationProvider] is not in use. - /// - /// Defaults to false. - final bool debugRequireGoRouteInformationProvider; - - final Map _nameToPath = {}; - - void _cacheNameToPath(String parentFullPath, List childRoutes) { - for (final GoRoute route in childRoutes) { - final String fullPath = concatenatePaths(parentFullPath, route.path); - - if (route.name != null) { - final String name = route.name!.toLowerCase(); - assert(!_nameToPath.containsKey(name), - 'duplication fullpaths for name "$name":${_nameToPath[name]}, $fullPath'); - _nameToPath[name] = fullPath; - } - - if (route.routes.isNotEmpty) { - _cacheNameToPath(fullPath, route.routes); - } - } - } - - /// Looks up the url location by a [GoRoute]'s name. - String namedLocation( - String name, { - Map params = const {}, - Map queryParams = const {}, - }) { - assert(() { - log.info('getting location for name: ' - '"$name"' - '${params.isEmpty ? '' : ', params: $params'}' - '${queryParams.isEmpty ? '' : ', queryParams: $queryParams'}'); - return true; - }()); - assert(_nameToPath.containsKey(name), 'unknown route name: $name'); - final String path = _nameToPath[name]!; - assert(() { - // Check that all required params are present. - final List paramNames = []; - patternToRegExp(path, paramNames); - for (final String paramName in paramNames) { - assert(params.containsKey(paramName), - 'missing param "$paramName" for $path'); - } - - // Check that there are no extra params - for (final String key in params.keys) { - assert(paramNames.contains(key), 'unknown param "$key" for $path'); - } - return true; - }()); - final Map encodedParams = { - for (final MapEntry param in params.entries) - param.key: Uri.encodeComponent(param.value) - }; - final String location = patternToPath(path, encodedParams); - return Uri(path: location, queryParameters: queryParams).toString(); - } - - /// Concatenates two paths. - /// - /// e.g: pathA = /a, pathB = c/d, concatenatePaths(pathA, pathB) = /a/c/d. - static String concatenatePaths(String parentPath, String childPath) { - // at the root, just return the path - if (parentPath.isEmpty) { - assert(childPath.startsWith('/')); - assert(childPath == '/' || !childPath.endsWith('/')); - return childPath; - } - - // not at the root, so append the parent path - assert(childPath.isNotEmpty); - assert(!childPath.startsWith('/')); - assert(!childPath.endsWith('/')); - return '${parentPath == '/' ? '' : parentPath}/$childPath'; - } - - /// for use by the Router architecture as part of the RouteInformationParser - @override - Future> parseRouteInformation( - RouteInformation routeInformation, - ) { - assert(() { - if (debugRequireGoRouteInformationProvider) { - assert( - routeInformation is DebugGoRouteInformation, - 'This GoRouteInformationParser needs to be used with ' - 'GoRouteInformationProvider, did you forget to pass in ' - 'GoRouter.routeInformationProvider to the Router constructor?'); - } - return true; - }()); - final List matches = - _getLocRouteMatchesWithRedirects(routeInformation); - // Use [SynchronousFuture] so that the initial url is processed - // synchronously and remove unwanted initial animations on deep-linking - return SynchronousFuture>(matches); - } - - List _getLocRouteMatchesWithRedirects( - RouteInformation routeInformation) { - // start redirecting from the initial location - List matches; - final String location = routeInformation.location!; - try { - // watch redirects for loops - final List redirects = [_canonicalUri(location)]; - bool redirected(String? redir) { - if (redir == null) { - return false; - } - - assert(() { - if (Uri.tryParse(redir) == null) { - throw _ParserError('invalid redirect: $redir'); - } - if (redirects.contains(redir)) { - throw _ParserError('redirect loop detected: ${[ - ...redirects, - redir - ].join(' => ')}'); - } - if (redirects.length > redirectLimit) { - throw _ParserError('too many redirects: ${[ - ...redirects, - redir - ].join(' => ')}'); - } - return true; - }()); - - redirects.add(redir); - assert(() { - log.info('redirecting to $redir'); - return true; - }()); - return true; - } - - // keep looping till we're done redirecting - while (true) { - final String loc = redirects.last; - - // check for top-level redirect - final Uri uri = Uri.parse(loc); - if (redirected( - topRedirect( - GoRouterState( - this, - location: loc, - name: null, // no name available at the top level - // trim the query params off the subloc to match route.redirect - subloc: uri.path, - // pass along the query params 'cuz that's all we have right now - queryParams: uri.queryParameters, - extra: routeInformation.state, - ), - ), - )) { - continue; - } - - // get stack of route matches - matches = _getLocRouteMatches(loc, routeInformation.state); - - // merge new params to keep params from previously matched paths, e.g. - // /family/:fid/person/:pid provides fid and pid to person/:pid - Map previouslyMatchedParams = {}; - for (final GoRouteMatch match in matches) { - assert( - !previouslyMatchedParams.keys.any(match.encodedParams.containsKey), - 'Duplicated parameter names', - ); - match.encodedParams.addAll(previouslyMatchedParams); - previouslyMatchedParams = match.encodedParams; - } - - // check top route for redirect - final GoRouteMatch top = matches.last; - if (redirected( - top.route.redirect( - GoRouterState( - this, - location: loc, - subloc: top.subloc, - name: top.route.name, - path: top.route.path, - fullpath: top.fullpath, - params: top.decodedParams, - queryParams: top.queryParams, - ), - ), - )) { - continue; - } - - // no more redirects! - break; - } - - // note that we need to catch it this way to get all the info, e.g. the - // file/line info for an error in an inline function impl, e.g. an inline - // `redirect` impl - // ignore: avoid_catches_without_on_clauses - } on _ParserError catch (err) { - // create a match that routes to the error page - final Exception error = Exception(err.message); - final Uri uri = Uri.parse(location); - matches = [ - GoRouteMatch( - subloc: uri.path, - fullpath: uri.path, - encodedParams: {}, - queryParams: uri.queryParameters, - extra: null, - error: error, - route: GoRoute( - path: location, - pageBuilder: (BuildContext context, GoRouterState state) { - throw UnimplementedError(); - }), - ), - ]; - } - assert(matches.isNotEmpty); - return matches; - } - - List _getLocRouteMatches(String location, Object? extra) { - final Uri uri = Uri.parse(location); - final List result = _getLocRouteRecursively( - loc: uri.path, - restLoc: uri.path, - routes: routes, - parentFullpath: '', - parentSubloc: '', - queryParams: uri.queryParameters, - extra: extra, - ); - - if (result.isEmpty) { - throw _ParserError('no routes for location: $location'); - } - - return result; - } - - static List _getLocRouteRecursively({ - required String loc, - required String restLoc, - required String parentSubloc, - required List routes, - required String parentFullpath, - required Map queryParams, - required Object? extra, - }) { - bool debugGatherAllMatches = false; - assert(() { - debugGatherAllMatches = true; - return true; - }()); - final List> result = >[]; - // find the set of matches at this level of the tree - for (final GoRoute route in routes) { - final String fullpath = concatenatePaths(parentFullpath, route.path); - final GoRouteMatch? match = GoRouteMatch.match( - route: route, - restLoc: restLoc, - parentSubloc: parentSubloc, - fullpath: fullpath, - queryParams: queryParams, - extra: extra, - ); - - if (match == null) { - continue; - } - if (match.subloc.toLowerCase() == loc.toLowerCase()) { - // If it is a complete match, then return the matched route - // NOTE: need a lower case match because subloc is canonicalized to match - // the path case whereas the location can be of any case and still match - result.add([match]); - } else if (route.routes.isEmpty) { - // If it is partial match but no sub-routes, bail. - continue; - } else { - // otherwise recurse - final String childRestLoc = - loc.substring(match.subloc.length + (match.subloc == '/' ? 0 : 1)); - assert(loc.startsWith(match.subloc)); - assert(restLoc.isNotEmpty); - - final List subRouteMatch = _getLocRouteRecursively( - loc: loc, - restLoc: childRestLoc, - parentSubloc: match.subloc, - routes: route.routes, - parentFullpath: fullpath, - queryParams: queryParams, - extra: extra, - ).toList(); - - // if there's no sub-route matches, there is no match for this - // location - if (subRouteMatch.isEmpty) { - continue; - } - result.add([match, ...subRouteMatch]); - } - // Should only reach here if there is a match. - if (debugGatherAllMatches) { - continue; - } else { - break; - } - } - - if (result.isEmpty) { - return []; - } - - // If there are multiple routes that match the location, returning the first one. - // To make predefined routes to take precedence over dynamic routes eg. '/:id' - // consider adding the dynamic route at the end of the routes - return result.first; - } - - void _debugLogKnownRoutes() { - log.info('known full paths for routes:'); - _debugLogFullPathsFor(routes, '', 0); - - if (_nameToPath.isNotEmpty) { - log.info('known full paths for route names:'); - for (final MapEntry e in _nameToPath.entries) { - log.info(' ${e.key} => ${e.value}'); - } - } - } - - void _debugLogFullPathsFor( - List routes, - String parentFullpath, - int depth, - ) { - for (final GoRoute route in routes) { - final String fullpath = concatenatePaths(parentFullpath, route.path); - assert(() { - log.info(' => ${''.padLeft(depth * 2)}$fullpath'); - return true; - }()); - _debugLogFullPathsFor(route.routes, fullpath, depth + 1); - } - } - - /// for use by the Router architecture as part of the RouteInformationParser - @override - RouteInformation restoreRouteInformation(List configuration) { - return RouteInformation( - location: configuration.last.fullUriString, - state: configuration.last.extra); - } -} - -/// Normalizes the location string. -String _canonicalUri(String loc) { - String canon = Uri.parse(loc).toString(); - canon = canon.endsWith('?') ? canon.substring(0, canon.length - 1) : canon; - - // remove trailing slash except for when you shouldn't, e.g. - // /profile/ => /profile - // / => / - // /login?from=/ => login?from=/ - canon = canon.endsWith('/') && canon != '/' && !canon.contains('?') - ? canon.substring(0, canon.length - 1) - : canon; - - // /login/?from=/ => /login?from=/ - // /?from=/ => /?from=/ - canon = canon.replaceFirst('/?', '?', 1); - - return canon; -} diff --git a/packages/go_router/lib/src/go_router.dart b/packages/go_router/lib/src/go_router.dart deleted file mode 100644 index 17a7af14145..00000000000 --- a/packages/go_router/lib/src/go_router.dart +++ /dev/null @@ -1,234 +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 'go_route.dart'; -import 'go_route_information_parser.dart'; -import 'go_route_information_provider.dart'; -import 'go_route_match.dart'; -import 'go_router_delegate.dart'; -import 'go_router_state.dart'; -import 'inherited_go_router.dart'; -import 'logging.dart'; -import 'path_strategy_nonweb.dart' - if (dart.library.html) 'path_strategy_web.dart'; -import 'typedefs.dart'; -import 'url_path_strategy.dart'; - -/// The top-level go router class. -/// -/// Create one of these to initialize your app's routing policy. -// ignore: prefer_mixin -class GoRouter extends ChangeNotifier with NavigatorObserver { - /// Default constructor to configure a GoRouter with a routes builder - /// and an error page builder. - GoRouter({ - required List routes, - GoRouterPageBuilder? errorPageBuilder, - GoRouterWidgetBuilder? errorBuilder, - GoRouterRedirect? redirect, - Listenable? refreshListenable, - int redirectLimit = 5, - bool routerNeglect = false, - String? initialLocation, - UrlPathStrategy? urlPathStrategy, - List? observers, - bool debugLogDiagnostics = false, - GoRouterNavigatorBuilder? navigatorBuilder, - String? restorationScopeId, - }) { - if (urlPathStrategy != null) { - setUrlPathStrategy(urlPathStrategy); - } - - setLogging(enabled: debugLogDiagnostics); - WidgetsFlutterBinding.ensureInitialized(); - - routeInformationParser = GoRouteInformationParser( - routes: routes, - topRedirect: redirect ?? (_) => null, - redirectLimit: redirectLimit, - debugRequireGoRouteInformationProvider: true, - ); - routeInformationProvider = GoRouteInformationProvider( - initialRouteInformation: RouteInformation( - location: _effectiveInitialLocation(initialLocation)), - refreshListenable: refreshListenable); - - routerDelegate = GoRouterDelegate( - routeInformationParser, - errorPageBuilder: errorPageBuilder, - errorBuilder: errorBuilder, - routerNeglect: routerNeglect, - observers: [ - ...observers ?? [], - this - ], - restorationScopeId: restorationScopeId, - // wrap the returned Navigator to enable GoRouter.of(context).go() et al, - // allowing the caller to wrap the navigator themselves - builderWithNav: - (BuildContext context, GoRouterState state, Navigator nav) => - InheritedGoRouter( - goRouter: this, - child: navigatorBuilder?.call(context, state, nav) ?? nav, - ), - ); - assert(() { - log.info('setting initial location $initialLocation'); - return true; - }()); - } - - /// The route information parser used by the go router. - late final GoRouteInformationParser routeInformationParser; - - /// The router delegate used by the go router. - late final GoRouterDelegate routerDelegate; - - /// The route information provider used by the go router. - late final GoRouteInformationProvider routeInformationProvider; - - /// Get the current location. - String get location => routerDelegate.currentConfiguration.last.fullUriString; - - /// Get a location from route name and parameters. - /// This is useful for redirecting to a named location. - String namedLocation( - String name, { - Map params = const {}, - Map queryParams = const {}, - }) => - routeInformationParser.namedLocation( - name, - params: params, - queryParams: queryParams, - ); - - /// Navigate to a URI location w/ optional query parameters, e.g. - /// `/family/f2/person/p1?color=blue` - void go(String location, {Object? extra}) { - assert(() { - log.info('going to $location'); - return true; - }()); - routeInformationProvider.value = - RouteInformation(location: location, state: extra); - } - - /// Navigate to a named route w/ optional parameters, e.g. - /// `name='person', params={'fid': 'f2', 'pid': 'p1'}` - /// Navigate to the named route. - void goNamed( - String name, { - Map params = const {}, - Map queryParams = const {}, - Object? extra, - }) => - go( - namedLocation(name, params: params, queryParams: queryParams), - extra: extra, - ); - - /// 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}) { - assert(() { - log.info('pushing $location'); - return true; - }()); - routeInformationParser - .parseRouteInformation( - DebugGoRouteInformation(location: location, state: extra)) - .then((List matches) { - routerDelegate.push(matches.last); - }); - } - - /// Push a named route onto the page stack w/ optional parameters, e.g. - /// `name='person', params={'fid': 'f2', 'pid': 'p1'}` - void pushNamed( - String name, { - Map params = const {}, - Map queryParams = const {}, - Object? extra, - }) => - push( - namedLocation(name, params: params, queryParams: queryParams), - extra: extra, - ); - - /// Returns `true` if there is more than 1 page on the stack. - bool canPop() => routerDelegate.canPop(); - - /// Pop the top page off the GoRouter's page stack. - void pop() { - assert(() { - log.info('popping $location'); - return true; - }()); - routerDelegate.pop(); - } - - /// Refresh the route. - void refresh() { - assert(() { - log.info('refreshing $location'); - return true; - }()); - routeInformationProvider.notifyListeners(); - } - - /// Set the app's URL path strategy (defaults to hash). call before runApp(). - static void setUrlPathStrategy(UrlPathStrategy strategy) => - setUrlPathStrategyImpl(strategy); - - /// Find the current GoRouter in the widget tree. - static GoRouter of(BuildContext context) { - final InheritedGoRouter? inherited = - context.dependOnInheritedWidgetOfExactType(); - assert(inherited != null, 'No GoRouter found in context'); - return inherited!.goRouter; - } - - /// The [Navigator] pushed `route`. - @override - void didPush(Route route, Route? previousRoute) => - notifyListeners(); - - /// The [Navigator] popped `route`. - @override - void didPop(Route route, Route? previousRoute) => - notifyListeners(); - - /// The [Navigator] removed `route`. - @override - void didRemove(Route route, Route? previousRoute) => - notifyListeners(); - - /// The [Navigator] replaced `oldRoute` with `newRoute`. - @override - void didReplace({Route? newRoute, Route? oldRoute}) => - notifyListeners(); - - @override - void dispose() { - routeInformationProvider.dispose(); - routerDelegate.dispose(); - super.dispose(); - } - - String _effectiveInitialLocation(String? initialLocation) { - final String platformDefault = - WidgetsBinding.instance.platformDispatcher.defaultRouteName; - if (initialLocation == null) { - return platformDefault; - } else if (platformDefault == '/') { - return initialLocation; - } else { - return platformDefault; - } - } -} diff --git a/packages/go_router/lib/src/go_route_information_provider.dart b/packages/go_router/lib/src/information_provider.dart similarity index 94% rename from packages/go_router/lib/src/go_route_information_provider.dart rename to packages/go_router/lib/src/information_provider.dart index 80c5a369b5a..cf85765b546 100644 --- a/packages/go_router/lib/src/go_route_information_provider.dart +++ b/packages/go_router/lib/src/information_provider.dart @@ -4,7 +4,6 @@ import 'package:flutter/services.dart'; import 'package:flutter/widgets.dart'; -import 'package:go_router/src/go_route_information_parser.dart'; /// The route information provider created by go_router class GoRouteInformationProvider extends RouteInformationProvider @@ -32,8 +31,7 @@ class GoRouteInformationProvider extends RouteInformationProvider (type == RouteInformationReportingType.none && _valueInEngine.location == routeInformation.location); SystemNavigator.selectMultiEntryHistory(); - // TODO(chunhtai): should report extra to the browser through state if - // possible. + // TODO(chunhtai): report extra to browser through state if possible SystemNavigator.routeInformationUpdated( location: routeInformation.location!, replace: replace, @@ -48,6 +46,7 @@ class GoRouteInformationProvider extends RouteInformationProvider state: _value.state, ); RouteInformation _value; + set value(RouteInformation other) { final bool shouldNotify = _value.location != other.location || _value.state != other.state; @@ -111,7 +110,7 @@ class GoRouteInformationProvider extends RouteInformationProvider } /// A debug class that is used for asserting the [GoRouteInformationProvider] is -/// in use with the [GoRouteInformationParser]. +/// in use with the [RouteInformationParser]. class DebugGoRouteInformation extends RouteInformation { /// Creates a [DebugGoRouteInformation] DebugGoRouteInformation({String? location, Object? state}) diff --git a/packages/go_router/lib/src/inherited_go_router.dart b/packages/go_router/lib/src/inherited_go_router.dart deleted file mode 100644 index b746612116f..00000000000 --- a/packages/go_router/lib/src/inherited_go_router.dart +++ /dev/null @@ -1,38 +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/foundation.dart'; -import 'package:flutter/widgets.dart'; - -import 'go_router.dart'; - -/// GoRouter implementation of InheritedWidget. -/// -/// Used for to find the current GoRouter in the widget tree. This is useful -/// when routing from anywhere in your app. -class InheritedGoRouter extends InheritedWidget { - /// Default constructor for the inherited go router. - const InheritedGoRouter({ - required Widget child, - required this.goRouter, - Key? key, - }) : super(child: child, key: key); - - /// The [GoRouter] that is made available to the widget tree. - final GoRouter goRouter; - - /// Used by the Router architecture as part of the InheritedWidget. - @override - // ignore: prefer_expression_function_bodies - bool updateShouldNotify(covariant InheritedGoRouter oldWidget) { - // avoid rebuilding the widget tree if the router has not changed - return goRouter != oldWidget.goRouter; - } - - @override - void debugFillProperties(DiagnosticPropertiesBuilder properties) { - super.debugFillProperties(properties); - properties.add(DiagnosticsProperty('goRouter', goRouter)); - } -} diff --git a/packages/go_router/lib/src/go_route_match.dart b/packages/go_router/lib/src/match.dart similarity index 92% rename from packages/go_router/lib/src/go_route_match.dart rename to packages/go_router/lib/src/match.dart index 35a03aa9c60..db7d186070c 100644 --- a/packages/go_router/lib/src/go_route_match.dart +++ b/packages/go_router/lib/src/match.dart @@ -4,16 +4,15 @@ import 'package:flutter/foundation.dart'; -import 'go_route.dart'; -import 'go_route_information_parser.dart'; -import 'path_parser.dart'; +import 'route.dart'; +import 'path_utils.dart'; /// Each GoRouteMatch instance represents an instance of a GoRoute for a /// specific portion of a location. -class GoRouteMatch { +class RouteMatch { /// Constructor for GoRouteMatch, each instance represents an instance of a /// GoRoute for a specific portion of a location. - GoRouteMatch({ + RouteMatch({ required this.route, required this.subloc, required this.fullpath, @@ -36,7 +35,7 @@ class GoRouteMatch { }()); // ignore: public_member_api_docs - static GoRouteMatch? match({ + static RouteMatch? match({ required GoRoute route, required String restLoc, // e.g. person/p1 required String parentSubloc, // e.g. /family/f2 @@ -53,9 +52,8 @@ class GoRouteMatch { final Map encodedParams = route.extractPathParams(match); final String pathLoc = patternToPath(route.path, encodedParams); - final String subloc = - GoRouteInformationParser.concatenatePaths(parentSubloc, pathLoc); - return GoRouteMatch( + final String subloc = concatenatePaths(parentSubloc, pathLoc); + return RouteMatch( route: route, subloc: subloc, fullpath: fullpath, diff --git a/packages/go_router/lib/src/matching.dart b/packages/go_router/lib/src/matching.dart new file mode 100644 index 00000000000..c94d9c9d2ec --- /dev/null +++ b/packages/go_router/lib/src/matching.dart @@ -0,0 +1,190 @@ +// 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 'configuration.dart'; +import 'match.dart'; +import 'path_utils.dart'; + +/// Converts a location into a list of [RouteMatch] objects +class RouteMatcher { + /// GoRouteMatcher constructor. + RouteMatcher(this.configuration); + + /// The route configuration + final RouteConfiguration configuration; + + /// Finds the routes that matched the given URL + RouteMatchList findMatch(String location, {Object? extra}) { + final String canonicalLocation = canonicalUri(location); + final List matches = + _getLocRouteMatches(canonicalLocation, extra); + return RouteMatchList(matches); + } + + List _getLocRouteMatches(String location, Object? extra) { + final Uri uri = Uri.parse(location); + final List result = _getLocRouteRecursively( + loc: uri.path, + restLoc: uri.path, + routes: configuration.routes, + parentFullpath: '', + parentSubloc: '', + queryParams: uri.queryParameters, + extra: extra, + ); + + if (result.isEmpty) { + throw MatcherError('no routes for location', location); + } + + return result; + } +} + +/// The list of [RouteMatch] objects. +class RouteMatchList { + /// GoRouteMatches constructor. + RouteMatchList(this._matches); + + /// Constructs an empty matches object + factory RouteMatchList.empty() => RouteMatchList([]); + + final List _matches; + + /// Returns true if there are no matches + bool get isEmpty => _matches.isEmpty; + + /// Returns true if there are matches + bool get isNotEmpty => _matches.isNotEmpty; + + /// The original URL that was matched + Uri get location => + _matches.isEmpty ? Uri() : Uri.parse(_matches.last.fullUriString); + + /// Pushes a match onto the list of matches + // TODO(johnpryan): deprecate this API when new route types are added + void push(covariant RouteMatch match) { + _matches.add(match); + } + + /// Removes the last match + void pop() { + _matches.removeLast(); + assert( + _matches.isNotEmpty, + 'You have popped the last page off of the stack,' + ' there are no pages left to show'); + } + + /// Returns true if [pop] can safely be called + bool canPop() { + return _matches.length > 1; + } + + /// An optional object provided by the app during navigation + Object? get extra => _matches.last.extra; + + /// The last matching route + RouteMatch get last => _matches.last; + + /// The route matches + List get matches => _matches; +} + +/// An error that occurred during matching +class MatcherError extends Error { + /// Constructs a MatcherError + MatcherError(String message, this.location) + : message = message + ': $location'; + + /// The error message + final String message; + + /// The location that failed to match + final String location; + + @override + String toString() { + return message; + } +} + +List _getLocRouteRecursively({ + required String loc, + required String restLoc, + required String parentSubloc, + required List routes, + required String parentFullpath, + required Map queryParams, + required Object? extra, +}) { + bool debugGatherAllMatches = false; + assert(() { + debugGatherAllMatches = true; + return true; + }()); + final List> result = >[]; + // find the set of matches at this level of the tree + for (final GoRoute route in routes) { + final String fullpath = concatenatePaths(parentFullpath, route.path); + final RouteMatch? match = RouteMatch.match( + route: route, + restLoc: restLoc, + parentSubloc: parentSubloc, + fullpath: fullpath, + queryParams: queryParams, + extra: extra, + ); + + if (match == null) { + continue; + } + if (match.subloc.toLowerCase() == loc.toLowerCase()) { + // If it is a complete match, then return the matched route + // NOTE: need a lower case match because subloc is canonicalized to match + // the path case whereas the location can be of any case and still match + result.add([match]); + } else if (route.routes.isEmpty) { + // If it is partial match but no sub-routes, bail. + continue; + } else { + // Otherwise, recurse + final String childRestLoc = + loc.substring(match.subloc.length + (match.subloc == '/' ? 0 : 1)); + assert(loc.startsWith(match.subloc)); + assert(restLoc.isNotEmpty); + + final List subRouteMatch = _getLocRouteRecursively( + loc: loc, + restLoc: childRestLoc, + parentSubloc: match.subloc, + routes: route.routes, + parentFullpath: fullpath, + queryParams: queryParams, + extra: extra, + ).toList(); + + // If there's no sub-route matches, there is no match for this location + if (subRouteMatch.isEmpty) { + continue; + } + result.add([match, ...subRouteMatch]); + } + // Should only reach here if there is a match. + if (debugGatherAllMatches) { + continue; + } else { + break; + } + } + + if (result.isEmpty) { + return []; + } + + // If there are multiple routes that match the location, returning the first one. + // To make predefined routes to take precedence over dynamic routes eg. '/:id' + // consider adding the dynamic route at the end of the routes + return result.first; +} diff --git a/packages/go_router/lib/src/go_router_error_page.dart b/packages/go_router/lib/src/misc/error_screen.dart similarity index 90% rename from packages/go_router/lib/src/go_router_error_page.dart rename to packages/go_router/lib/src/misc/error_screen.dart index f9c1e68b0f2..a965a87acb0 100644 --- a/packages/go_router/lib/src/go_router_error_page.dart +++ b/packages/go_router/lib/src/misc/error_screen.dart @@ -2,15 +2,13 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -// ignore_for_file: diagnostic_describe_all_properties - import 'package:flutter/widgets.dart'; -import '../go_router.dart'; +import '../../go_router.dart'; /// Default error page implementation for WidgetsApp. -class GoRouterErrorScreen extends StatelessWidget { +class ErrorScreen extends StatelessWidget { /// Provide an exception to this page for it to be displayed. - const GoRouterErrorScreen(this.error, {Key? key}) : super(key: key); + const ErrorScreen(this.error, {Key? key}) : super(key: key); /// The exception to be displayed. final Exception? error; diff --git a/packages/go_router/lib/src/go_router_refresh_stream.dart b/packages/go_router/lib/src/misc/refresh_stream.dart similarity index 69% rename from packages/go_router/lib/src/go_router_refresh_stream.dart rename to packages/go_router/lib/src/misc/refresh_stream.dart index 78606409cdd..8262ef70be3 100644 --- a/packages/go_router/lib/src/go_router_refresh_stream.dart +++ b/packages/go_router/lib/src/misc/refresh_stream.dart @@ -7,11 +7,7 @@ import 'dart:async'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -import 'go_router.dart'; - -/// This class can be used to make `refreshListenable` react to events in the -/// the provided stream. This allows you to listen to stream based state -/// management solutions like for example BLoC. +/// Converts a [Stream] into a [Listenable] /// /// {@tool snippet} /// Typical usage is as follows: @@ -22,12 +18,12 @@ import 'go_router.dart'; /// ); /// ``` /// {@end-tool} -class GoRouterRefreshStream extends ChangeNotifier { - /// Creates a [GoRouterRefreshStream]. +class StreamListenable extends ChangeNotifier { + /// Creates a [StreamListenable]. /// /// Every time the [stream] receives an event the [GoRouter] will refresh its /// current route. - GoRouterRefreshStream(Stream stream) { + StreamListenable(Stream stream) { notifyListeners(); _subscription = stream.asBroadcastStream().listen( (dynamic _) => notifyListeners(), diff --git a/packages/go_router/lib/src/go_router_cupertino.dart b/packages/go_router/lib/src/pages/cupertino.dart similarity index 90% rename from packages/go_router/lib/src/go_router_cupertino.dart rename to packages/go_router/lib/src/pages/cupertino.dart index 0314718be17..9289d185bb9 100644 --- a/packages/go_router/lib/src/go_router_cupertino.dart +++ b/packages/go_router/lib/src/pages/cupertino.dart @@ -5,7 +5,7 @@ // ignore_for_file: diagnostic_describe_all_properties import 'package:flutter/cupertino.dart'; -import '../go_router.dart'; +import '../../go_router.dart'; /// Checks for CupertinoApp in the widget tree. bool isCupertinoApp(Element elem) => @@ -28,9 +28,9 @@ CupertinoPage pageBuilderForCupertinoApp({ ); /// Default error page implementation for Cupertino. -class GoRouterCupertinoErrorScreen extends StatelessWidget { +class CupertinoErrorScreen extends StatelessWidget { /// Provide an exception to this page for it to be displayed. - const GoRouterCupertinoErrorScreen(this.error, {Key? key}) : super(key: key); + const CupertinoErrorScreen(this.error, {Key? key}) : super(key: key); /// The exception to be displayed. final Exception? error; diff --git a/packages/go_router/lib/src/custom_transition_page.dart b/packages/go_router/lib/src/pages/custom_transition_page.dart similarity index 100% rename from packages/go_router/lib/src/custom_transition_page.dart rename to packages/go_router/lib/src/pages/custom_transition_page.dart diff --git a/packages/go_router/lib/src/go_router_material.dart b/packages/go_router/lib/src/pages/material.dart similarity index 89% rename from packages/go_router/lib/src/go_router_material.dart rename to packages/go_router/lib/src/pages/material.dart index 1b01920a266..1d24c8c2daf 100644 --- a/packages/go_router/lib/src/go_router_material.dart +++ b/packages/go_router/lib/src/pages/material.dart @@ -5,7 +5,7 @@ // ignore_for_file: diagnostic_describe_all_properties import 'package:flutter/material.dart'; -import '../go_router.dart'; +import '../../go_router.dart'; /// Checks for MaterialApp in the widget tree. bool isMaterialApp(Element elem) => @@ -28,9 +28,9 @@ MaterialPage pageBuilderForMaterialApp({ ); /// Default error page implementation for Material. -class GoRouterMaterialErrorScreen extends StatelessWidget { +class MaterialErrorScreen extends StatelessWidget { /// Provide an exception to this page for it to be displayed. - const GoRouterMaterialErrorScreen(this.error, {Key? key}) : super(key: key); + const MaterialErrorScreen(this.error, {Key? key}) : super(key: key); /// The exception to be displayed. final Exception? error; diff --git a/packages/go_router/lib/src/parser.dart b/packages/go_router/lib/src/parser.dart new file mode 100644 index 00000000000..48cf9de64cf --- /dev/null +++ b/packages/go_router/lib/src/parser.dart @@ -0,0 +1,117 @@ +// 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/foundation.dart'; +import 'package:flutter/widgets.dart'; + +import 'configuration.dart'; +import 'information_provider.dart'; +import 'match.dart'; +import 'matching.dart'; +import 'redirection.dart'; + +/// Converts between incoming URLs and a [RouteMatchList] using [RouteMatcher]. Also performs redirection using [RouteRedirector] +class GoRouterInformationParser extends RouteInformationParser { + /// Creates a [GoRouterInformationParser]. + GoRouterInformationParser({ + required this.configuration, + this.debugRequireGoRouteInformationProvider = false, + }) : matcher = RouteMatcher(configuration), + redirector = redirect; + + /// The route configuration for the app. + RouteConfiguration configuration; + + /// The route matcher + RouteMatcher matcher; + + /// The route redirector + RouteRedirector redirector; + + /// A debug property to assert [GoRouteInformationProvider] is in use along + /// with this parser. + /// + /// An assertion error will be thrown if this property set to true and the + /// [GoRouteInformationProvider] is not in use. + /// + /// Defaults to false. + final bool debugRequireGoRouteInformationProvider; + + /// for use by the Router architecture as part of the RouteInformationParser + @override + Future parseRouteInformation( + RouteInformation routeInformation, + ) { + assert(() { + if (debugRequireGoRouteInformationProvider) { + assert( + routeInformation is DebugGoRouteInformation, + 'This GoRouteInformationParser needs to be used with ' + 'GoRouteInformationProvider, did you forget to pass in ' + 'GoRouter.routeInformationProvider to the Router constructor?'); + } + return true; + }()); + try { + late final RouteMatchList initialMatches; + try { + initialMatches = matcher.findMatch(routeInformation.location!, + extra: routeInformation.state); + } on MatcherError { + // If there is a matching error for the initial location, we should still + // try to process the top-level redirects. + initialMatches = RouteMatchList.empty(); + } + final RouteMatchList matches = redirector( + initialMatches, configuration, matcher, + extra: routeInformation.state); + if (matches.isEmpty) { + return SynchronousFuture(_errorScreen( + Uri.parse(routeInformation.location!), + MatcherError('no routes for location', routeInformation.location!) + .toString())); + } + + // Use [SynchronousFuture] so that the initial url is processed + // synchronously and remove unwanted initial animations on deep-linking + return SynchronousFuture(matches); + } on RedirectionError catch (e) { + final Uri uri = e.location; + return SynchronousFuture(_errorScreen(uri, e.message)); + } on MatcherError catch (e) { + final Uri uri = Uri.parse(e.location); + return SynchronousFuture(_errorScreen(uri, e.message)); + } + } + + /// for use by the Router architecture as part of the RouteInformationParser + @override + RouteInformation restoreRouteInformation(RouteMatchList configuration) { + return RouteInformation( + location: configuration.location.toString(), + state: configuration.extra, + ); + } + + /// Creates a match that routes to the error page + RouteMatchList _errorScreen(Uri uri, String errorMessage) { + final Exception error = Exception(errorMessage); + return RouteMatchList([ + RouteMatch( + subloc: uri.path, + fullpath: uri.path, + encodedParams: {}, + queryParams: uri.queryParameters, + extra: null, + error: error, + route: GoRoute( + path: uri.toString(), + pageBuilder: (BuildContext context, GoRouterState state) { + throw UnimplementedError(); + }, + ), + ), + ]); + } +} diff --git a/packages/go_router/lib/src/path_parser.dart b/packages/go_router/lib/src/path_utils.dart similarity index 74% rename from packages/go_router/lib/src/path_parser.dart rename to packages/go_router/lib/src/path_utils.dart index bf126330921..b10a6cc1077 100644 --- a/packages/go_router/lib/src/path_parser.dart +++ b/packages/go_router/lib/src/path_utils.dart @@ -94,3 +94,41 @@ Map extractPathParameters( parameters[i]: match.namedGroup(parameters[i])! }; } + +/// Concatenates two paths. +/// +/// e.g: pathA = /a, pathB = c/d, concatenatePaths(pathA, pathB) = /a/c/d. +String concatenatePaths(String parentPath, String childPath) { + // at the root, just return the path + if (parentPath.isEmpty) { + assert(childPath.startsWith('/')); + assert(childPath == '/' || !childPath.endsWith('/')); + return childPath; + } + + // not at the root, so append the parent path + assert(childPath.isNotEmpty); + assert(!childPath.startsWith('/')); + assert(!childPath.endsWith('/')); + return '${parentPath == '/' ? '' : parentPath}/$childPath'; +} + +/// Normalizes the location string. +String canonicalUri(String loc) { + String canon = Uri.parse(loc).toString(); + canon = canon.endsWith('?') ? canon.substring(0, canon.length - 1) : canon; + + // remove trailing slash except for when you shouldn't, e.g. + // /profile/ => /profile + // / => / + // /login?from=/ => login?from=/ + canon = canon.endsWith('/') && canon != '/' && !canon.contains('?') + ? canon.substring(0, canon.length - 1) + : canon; + + // /login/?from=/ => /login?from=/ + // /?from=/ => /?from=/ + canon = canon.replaceFirst('/?', '?', 1); + + return canon; +} diff --git a/packages/go_router/lib/src/platform.dart b/packages/go_router/lib/src/platform.dart new file mode 100644 index 00000000000..db4c7b8a8a6 --- /dev/null +++ b/packages/go_router/lib/src/platform.dart @@ -0,0 +1,8 @@ +// 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. + +// TODO(johnpryan): Remove this API +export 'platform/path_strategy_nonweb.dart' + if (dart.library.html) 'platform/path_strategy_web.dart'; +export 'platform/url_path_strategy.dart'; diff --git a/packages/go_router/lib/src/path_strategy_nonweb.dart b/packages/go_router/lib/src/platform/path_strategy_nonweb.dart similarity index 90% rename from packages/go_router/lib/src/path_strategy_nonweb.dart rename to packages/go_router/lib/src/platform/path_strategy_nonweb.dart index 3b9240031f4..4bd9d8b2b4e 100644 --- a/packages/go_router/lib/src/path_strategy_nonweb.dart +++ b/packages/go_router/lib/src/platform/path_strategy_nonweb.dart @@ -5,6 +5,7 @@ import 'url_path_strategy.dart'; /// no-op implementation of the URL path strategy for non-web target platforms +// TODO(johnpryan): Remove this API void setUrlPathStrategyImpl(UrlPathStrategy strategy) { // no-op } diff --git a/packages/go_router/lib/src/path_strategy_web.dart b/packages/go_router/lib/src/platform/path_strategy_web.dart similarity index 100% rename from packages/go_router/lib/src/path_strategy_web.dart rename to packages/go_router/lib/src/platform/path_strategy_web.dart diff --git a/packages/go_router/lib/src/url_path_strategy.dart b/packages/go_router/lib/src/platform/url_path_strategy.dart similarity index 100% rename from packages/go_router/lib/src/url_path_strategy.dart rename to packages/go_router/lib/src/platform/url_path_strategy.dart diff --git a/packages/go_router/lib/src/redirection.dart b/packages/go_router/lib/src/redirection.dart new file mode 100644 index 00000000000..9c35c2981ff --- /dev/null +++ b/packages/go_router/lib/src/redirection.dart @@ -0,0 +1,156 @@ +// 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 'configuration.dart'; +import 'logging.dart'; +import 'match.dart'; +import 'matching.dart'; + +/// A GoRouter redirector function. +// TODO(johnpryan): make redirector async (#105808) +typedef RouteRedirector = RouteMatchList Function(RouteMatchList matches, + RouteConfiguration configuration, RouteMatcher matcher, + {Object? extra}); + +/// Processes redirects by returning a new [RouteMatchList] representing the new +/// location. +RouteMatchList redirect(RouteMatchList prevMatchList, + RouteConfiguration configuration, RouteMatcher matcher, + {Object? extra}) { + RouteMatchList matches; + + // Store each redirect to detect loops + final List redirects = [prevMatchList]; + + // Adds the redirect to the list of redirects if it is valid. + bool redirected(RouteMatchList newRedirect) { + if (newRedirect == null) { + return false; + } + + // Verify that the redirect can be parsed and is not already + // in the list of redirects + assert(() { + if (redirects.contains(newRedirect)) { + throw RedirectionError( + 'redirect loop detected', + [...redirects, newRedirect], + prevMatchList.location); + } + if (redirects.length > configuration.redirectLimit) { + throw RedirectionError( + 'too many redirects', + [...redirects, newRedirect], + prevMatchList.location); + } + return true; + }()); + + redirects.add(newRedirect); + + assert(() { + log.info('redirecting to $newRedirect'); + return true; + }()); + return true; + } + + // Keep looping until redirecting is done + while (true) { + final RouteMatchList currentMatches = redirects.last; + + // Check for top-level redirect + final Uri uri = currentMatches.location; + final String? topRedirectLocation = configuration.topRedirect( + GoRouterState( + configuration, + location: currentMatches.location.toString(), + name: null, + // No name available at the top level trim the query params off the + // sub-location to match route.redirect + subloc: uri.path, + queryParams: uri.queryParameters, + extra: extra, + ), + ); + + // If the new location is null, keep the matches the same as before. + if (topRedirectLocation != null) { + final RouteMatchList newMatch = matcher.findMatch(topRedirectLocation); + + if (redirected(newMatch)) { + continue; + } else { + matches = newMatch; + } + } else { + matches = currentMatches; + } + + // Merge new params to keep params from previously matched paths, e.g. + // /users/:userId/book/:bookId provides userId and bookId to book/:bookId + Map previouslyMatchedParams = {}; + for (final RouteMatch match in currentMatches.matches) { + assert( + !previouslyMatchedParams.keys.any(match.encodedParams.containsKey), + 'Duplicated parameter names', + ); + match.encodedParams.addAll(previouslyMatchedParams); + previouslyMatchedParams = match.encodedParams; + } + + // check top route for redirect + final RouteMatch? top = matches.isNotEmpty ? matches.last : null; + if (top == null) { + break; + } + final String? topRouteLocation = top.route.redirect( + GoRouterState( + configuration, + location: currentMatches.location.toString(), + subloc: top.subloc, + name: top.route.name, + path: top.route.path, + fullpath: top.fullpath, + params: top.decodedParams, + queryParams: top.queryParams, + ), + ); + + if (topRouteLocation == null) { + break; + } + + final RouteMatchList newMatchList = matcher.findMatch(topRouteLocation); + if (redirected(newMatchList)) { + continue; + } + + break; + } + return matches; +} + +/// A configuration error detected while processing redirects. +class RedirectionError extends Error implements UnsupportedError { + /// RedirectionError constructor + RedirectionError(this.message, this.matches, this.location); + + /// The matches that were found while processing redirects. + final List matches; + + @override + final String message; + + /// The location that was originally navigated to, before redirection began. + final Uri location; + + @override + String toString() => + super.toString() + + [ + ...matches.map( + (RouteMatchList routeMatches) => routeMatches.location.toString()), + ].join(' => '); +} diff --git a/packages/go_router/lib/src/go_route.dart b/packages/go_router/lib/src/route.dart similarity index 97% rename from packages/go_router/lib/src/go_route.dart rename to packages/go_router/lib/src/route.dart index 832c4e8ae87..e82f0562350 100644 --- a/packages/go_router/lib/src/go_route.dart +++ b/packages/go_router/lib/src/route.dart @@ -5,9 +5,8 @@ import 'package:collection/collection.dart'; import 'package:flutter/widgets.dart'; -import 'custom_transition_page.dart'; -import 'go_router_state.dart'; -import 'path_parser.dart'; +import 'path_utils.dart'; +import 'state.dart'; import 'typedefs.dart'; /// A declarative mapping between a route path and a page builder. @@ -27,7 +26,8 @@ class GoRoute { pageBuilder != null || builder != _invalidBuilder || redirect != _noRedirection, - 'GoRoute builder parameter not set\nSee gorouter.dev/redirection#considerations for details') { + 'GoRoute builder parameter not set\n' + 'See gorouter.dev/redirection#considerations for details') { // cache the path regexp and parameters _pathRE = patternToRegExp(path, _pathParams); diff --git a/packages/go_router/lib/src/routing.dart b/packages/go_router/lib/src/routing.dart new file mode 100644 index 00000000000..71dc60badf3 --- /dev/null +++ b/packages/go_router/lib/src/routing.dart @@ -0,0 +1,7 @@ +// 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. + +export 'delegate.dart'; +export 'information_provider.dart'; +export 'parser.dart'; diff --git a/packages/go_router/lib/src/go_router_state.dart b/packages/go_router/lib/src/state.dart similarity index 88% rename from packages/go_router/lib/src/go_router_state.dart rename to packages/go_router/lib/src/state.dart index 9db78342fdc..9b30da1365f 100644 --- a/packages/go_router/lib/src/go_router_state.dart +++ b/packages/go_router/lib/src/state.dart @@ -4,13 +4,13 @@ import 'package:flutter/foundation.dart'; -import 'go_route_information_parser.dart'; +import 'configuration.dart'; /// The route state during routing. class GoRouterState { /// Default constructor for creating route state during routing. GoRouterState( - this._delegate, { + this._configuration, { required this.location, required this.subloc, required this.name, @@ -29,8 +29,8 @@ class GoRouterState { : subloc), assert((path ?? '').isEmpty == (fullpath ?? '').isEmpty); - // TODO(chunhtai): remove this once namedLocation is removed from go_router. - final GoRouteInformationParser _delegate; + // TODO(johnpryan): remove once namedLocation is removed from go_router. + final RouteConfiguration _configuration; /// The full location of the route, e.g. /family/f2/person/p1 final String location; @@ -64,12 +64,13 @@ class GoRouterState { /// Get a location from route name and parameters. /// This is useful for redirecting to a named location. + // TODO(johnpryan): deprecate namedLocation API String namedLocation( String name, { Map params = const {}, Map queryParams = const {}, }) { - return _delegate.namedLocation(name, + return _configuration.namedLocation(name, params: params, queryParams: queryParams); } } diff --git a/packages/go_router/lib/src/route_data.dart b/packages/go_router/lib/src/typed_routing.dart similarity index 98% rename from packages/go_router/lib/src/route_data.dart rename to packages/go_router/lib/src/typed_routing.dart index f9a95e8b3c1..0f9f7b50f93 100644 --- a/packages/go_router/lib/src/route_data.dart +++ b/packages/go_router/lib/src/typed_routing.dart @@ -6,8 +6,8 @@ import 'package:flutter/widgets.dart'; import 'package:meta/meta.dart'; import 'package:meta/meta_meta.dart'; -import 'go_route.dart'; -import 'go_router_state.dart'; +import 'route.dart'; +import 'state.dart'; /// Baseclass for supporting /// [typed routing](https://gorouter.dev/typed-routing). diff --git a/packages/go_router/lib/src/typedefs.dart b/packages/go_router/lib/src/typedefs.dart index bec6eaa0fcf..ead151cf1e4 100644 --- a/packages/go_router/lib/src/typedefs.dart +++ b/packages/go_router/lib/src/typedefs.dart @@ -2,42 +2,35 @@ // 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 'package:flutter/material.dart'; -import 'go_route_match.dart'; -import 'go_router_state.dart'; - -/// Signature of a go router builder function with matchers. -typedef GoRouterBuilderWithMatches = Widget Function( - BuildContext context, - Iterable matches, -); - -/// Signature of a go router builder function with navigator. -typedef GoRouterBuilderWithNav = Widget Function( - BuildContext context, - GoRouterState state, - Navigator navigator, -); - -/// The signature of the page builder callback for a matched GoRoute. -typedef GoRouterPageBuilder = Page Function( - BuildContext context, - GoRouterState state, -); +import 'configuration.dart'; /// The signature of the widget builder callback for a matched GoRoute. typedef GoRouterWidgetBuilder = Widget Function( - BuildContext context, - GoRouterState state, -); + BuildContext context, + GoRouterState state, + ); -/// The signature of the redirect callback. -typedef GoRouterRedirect = String? Function(GoRouterState state); +/// The signature of the page builder callback for a matched GoRoute. +typedef GoRouterPageBuilder = Page Function( + BuildContext context, + GoRouterState state, + ); /// The signature of the navigatorBuilder callback. typedef GoRouterNavigatorBuilder = Widget Function( - BuildContext context, - GoRouterState state, - Widget child, -); + BuildContext context, + GoRouterState state, + Widget child, + ); + +/// Signature of a go router builder function with navigator. +typedef GoRouterBuilderWithNav = Widget Function( + BuildContext context, + GoRouterState state, + Navigator navigator, + ); + +/// The signature of the redirect callback. +typedef GoRouterRedirect = String? Function(GoRouterState state); \ No newline at end of file diff --git a/packages/go_router/pubspec.yaml b/packages/go_router/pubspec.yaml index f122200e755..422a54c3253 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: 4.1.0 +version: 4.1.1 repository: https://github.com/flutter/packages/tree/main/packages/go_router issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+go_router%22 diff --git a/packages/go_router/test/go_router_cupertino_test.dart b/packages/go_router/test/cupertino_test.dart similarity index 92% rename from packages/go_router/test/go_router_cupertino_test.dart rename to packages/go_router/test/cupertino_test.dart index 594a865000e..4413d6a3dd2 100644 --- a/packages/go_router/test/go_router_cupertino_test.dart +++ b/packages/go_router/test/cupertino_test.dart @@ -5,9 +5,9 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:go_router/src/go_router_cupertino.dart'; +import 'package:go_router/src/pages/cupertino.dart'; -import 'error_screen_helpers.dart'; +import 'helpers/error_screen_helpers.dart'; void main() { group('isCupertinoApp', () { @@ -63,7 +63,7 @@ void main() { 'shows "page not found" by default', testPageNotFound( widget: const CupertinoApp( - home: GoRouterCupertinoErrorScreen(null), + home: CupertinoErrorScreen(null), ), ), ); @@ -74,7 +74,7 @@ void main() { testPageShowsExceptionMessage( exception: exception, widget: CupertinoApp( - home: GoRouterCupertinoErrorScreen(exception), + home: CupertinoErrorScreen(exception), ), ), ); @@ -85,7 +85,7 @@ void main() { buttonFinder: find.byType(CupertinoButton), appRouterBuilder: cupertinoAppRouterBuilder, widget: const CupertinoApp( - home: GoRouterCupertinoErrorScreen(null), + home: CupertinoErrorScreen(null), ), ), ); diff --git a/packages/go_router/test/go_router_delegate_test.dart b/packages/go_router/test/delegate_test.dart similarity index 85% rename from packages/go_router/test/go_router_delegate_test.dart rename to packages/go_router/test/delegate_test.dart index 73592a27db5..81493f00b62 100644 --- a/packages/go_router/test/go_router_delegate_test.dart +++ b/packages/go_router/test/delegate_test.dart @@ -5,8 +5,8 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:go_router/go_router.dart'; -import 'package:go_router/src/go_route_match.dart'; -import 'package:go_router/src/go_router_error_page.dart'; +import 'package:go_router/src/match.dart'; +import 'package:go_router/src/misc/error_screen.dart'; Future createGoRouter( WidgetTester tester, { @@ -18,7 +18,7 @@ Future createGoRouter( GoRoute(path: '/', builder: (_, __) => const DummyStatefulWidget()), GoRoute( path: '/error', - builder: (_, __) => const GoRouterErrorScreen(null), + builder: (_, __) => const ErrorScreen(null), ), ], refreshListenable: refreshListenable, @@ -37,10 +37,11 @@ void main() { ..push('/error'); goRouter.routerDelegate.addListener(expectAsync0(() {})); - final GoRouteMatch last = goRouter.routerDelegate.matches.last; + final RouteMatch last = + goRouter.routerDelegate.matches.matches.last; goRouter.routerDelegate.pop(); - expect(goRouter.routerDelegate.matches.length, 1); - expect(goRouter.routerDelegate.matches.contains(last), false); + expect(goRouter.routerDelegate.matches.matches.length, 1); + expect(goRouter.routerDelegate.matches.matches.contains(last), false); }); testWidgets('throws when it pops more than matches count', @@ -62,7 +63,7 @@ void main() { (WidgetTester tester) async { final GoRouter goRouter = await createGoRouter(tester); - expect(goRouter.routerDelegate.matches.length, 1); + expect(goRouter.routerDelegate.matches.matches.length, 1); expect(goRouter.routerDelegate.canPop(), false); }, ); @@ -72,7 +73,7 @@ void main() { final GoRouter goRouter = await createGoRouter(tester) ..push('/error'); - expect(goRouter.routerDelegate.matches.length, 2); + expect(goRouter.routerDelegate.matches.matches.length, 2); expect(goRouter.routerDelegate.canPop(), true); }, ); @@ -91,6 +92,7 @@ void main() { class FakeRefreshListenable extends ChangeNotifier { bool unsubscribed = false; + @override void removeListener(VoidCallback listener) { unsubscribed = true; diff --git a/packages/go_router/test/go_router_error_page_test.dart b/packages/go_router/test/error_page_test.dart similarity index 86% rename from packages/go_router/test/go_router_error_page_test.dart rename to packages/go_router/test/error_page_test.dart index 3e5b2fdc7b1..5028dcc7af8 100644 --- a/packages/go_router/test/go_router_error_page_test.dart +++ b/packages/go_router/test/error_page_test.dart @@ -4,16 +4,16 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:go_router/src/go_router_error_page.dart'; +import 'package:go_router/src/misc/error_screen.dart'; -import 'error_screen_helpers.dart'; +import 'helpers/error_screen_helpers.dart'; void main() { testWidgets( 'shows "page not found" by default', testPageNotFound( widget: widgetsAppBuilder( - home: const GoRouterErrorScreen(null), + home: const ErrorScreen(null), ), ), ); @@ -24,7 +24,7 @@ void main() { testPageShowsExceptionMessage( exception: exception, widget: widgetsAppBuilder( - home: GoRouterErrorScreen(exception), + home: ErrorScreen(exception), ), ), ); @@ -35,7 +35,7 @@ void main() { buttonFinder: find.byWidgetPredicate((Widget widget) => widget is GestureDetector), widget: widgetsAppBuilder( - home: const GoRouterErrorScreen(null), + home: const ErrorScreen(null), ), ), ); diff --git a/packages/go_router/test/go_router_test.dart b/packages/go_router/test/go_router_test.dart index bd7e3e8fb9f..e161fd483e9 100644 --- a/packages/go_router/test/go_router_test.dart +++ b/packages/go_router/test/go_router_test.dart @@ -10,10 +10,9 @@ import 'package:flutter/material.dart'; import 'package:flutter/src/foundation/diagnostics.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:go_router/go_router.dart'; -import 'package:go_router/src/go_route_match.dart'; -import 'package:go_router/src/go_router_delegate.dart'; -import 'package:go_router/src/go_router_error_page.dart'; -import 'package:go_router/src/typedefs.dart'; +import 'package:go_router/src/delegate.dart'; +import 'package:go_router/src/match.dart'; +import 'package:go_router/src/misc/error_screen.dart'; import 'package:logging/logging.dart'; const bool enableLogs = true; @@ -33,7 +32,7 @@ void main() { ]; final GoRouter router = await _router(routes, tester); - final List matches = router.routerDelegate.matches; + final List matches = router.routerDelegate.matches.matches; expect(matches, hasLength(1)); expect(matches.first.fullpath, '/'); expect(router.screenFor(matches.first).runtimeType, HomeScreen); @@ -48,7 +47,7 @@ void main() { final GoRouter router = await _router(routes, tester); router.go('/'); - final List matches = router.routerDelegate.matches; + final List matches = router.routerDelegate.matches.matches; expect(matches, hasLength(1)); expect(matches.first.fullpath, '/'); expect(router.screenFor(matches.first).runtimeType, DummyScreen); @@ -108,9 +107,9 @@ void main() { final GoRouter router = await _router(routes, tester); router.go('/foo'); await tester.pumpAndSettle(); - final List matches = router.routerDelegate.matches; + final List matches = router.routerDelegate.matches.matches; expect(matches, hasLength(1)); - expect(router.screenFor(matches.first).runtimeType, ErrorScreen); + expect(router.screenFor(matches.first).runtimeType, TestErrorScreen); }); testWidgets('match 2nd top level route', (WidgetTester tester) async { @@ -127,7 +126,7 @@ void main() { final GoRouter router = await _router(routes, tester); router.go('/login'); - final List matches = router.routerDelegate.matches; + final List matches = router.routerDelegate.matches.matches; expect(matches, hasLength(1)); expect(matches.first.subloc, '/login'); expect(router.screenFor(matches.first).runtimeType, LoginScreen); @@ -155,7 +154,7 @@ void main() { final GoRouter router = await _router(routes, tester); router.go('/login'); - final List matches = router.routerDelegate.matches; + final List matches = router.routerDelegate.matches.matches; expect(matches, hasLength(1)); expect(matches.first.subloc, '/login'); expect(router.screenFor(matches.first).runtimeType, LoginScreen); @@ -178,7 +177,8 @@ void main() { final GoRouter router = await _router(routes, tester); router.go('/login/'); - final List matches = router.routerDelegate.matches; + final List matches = router.routerDelegate.matches.matches; + print(matches); expect(matches, hasLength(1)); expect(matches.first.subloc, '/login'); expect(router.screenFor(matches.first).runtimeType, LoginScreen); @@ -193,7 +193,7 @@ void main() { final GoRouter router = await _router(routes, tester); router.go('/profile/'); - final List matches = router.routerDelegate.matches; + final List matches = router.routerDelegate.matches.matches; expect(matches, hasLength(1)); expect(matches.first.subloc, '/profile/foo'); expect(router.screenFor(matches.first).runtimeType, DummyScreen); @@ -208,7 +208,7 @@ void main() { final GoRouter router = await _router(routes, tester); router.go('/profile/?bar=baz'); - final List matches = router.routerDelegate.matches; + final List matches = router.routerDelegate.matches.matches; expect(matches, hasLength(1)); expect(matches.first.subloc, '/profile/foo'); expect(router.screenFor(matches.first).runtimeType, DummyScreen); @@ -232,7 +232,7 @@ void main() { final GoRouter router = await _router(routes, tester); router.go('/login'); - final List matches = router.routerDelegate.matches; + final List matches = router.routerDelegate.matches.matches; expect(matches.length, 2); expect(matches.first.subloc, '/'); expect(router.screenFor(matches.first).runtimeType, HomeScreen); @@ -270,7 +270,7 @@ void main() { final GoRouter router = await _router(routes, tester); { - final List matches = router.routerDelegate.matches; + final List matches = router.routerDelegate.matches.matches; expect(matches, hasLength(1)); expect(matches.first.fullpath, '/'); expect(router.screenFor(matches.first).runtimeType, HomeScreen); @@ -278,7 +278,7 @@ void main() { router.go('/login'); { - final List matches = router.routerDelegate.matches; + final List matches = router.routerDelegate.matches.matches; expect(matches.length, 2); expect(matches.first.subloc, '/'); expect(router.screenFor(matches.first).runtimeType, HomeScreen); @@ -288,7 +288,7 @@ void main() { router.go('/family/f2'); { - final List matches = router.routerDelegate.matches; + final List matches = router.routerDelegate.matches.matches; expect(matches.length, 2); expect(matches.first.subloc, '/'); expect(router.screenFor(matches.first).runtimeType, HomeScreen); @@ -298,7 +298,7 @@ void main() { router.go('/family/f2/person/p1'); { - final List matches = router.routerDelegate.matches; + final List matches = router.routerDelegate.matches.matches; expect(matches.length, 3); expect(matches.first.subloc, '/'); expect(router.screenFor(matches.first).runtimeType, HomeScreen); @@ -345,17 +345,17 @@ void main() { final GoRouter router = await _router(routes, tester); router.go('/bar'); - List matches = router.routerDelegate.matches; + List matches = router.routerDelegate.matches.matches; expect(matches, hasLength(2)); expect(router.screenFor(matches[1]).runtimeType, Page1Screen); router.go('/foo/bar'); - matches = router.routerDelegate.matches; + matches = router.routerDelegate.matches.matches; expect(matches, hasLength(2)); expect(router.screenFor(matches[1]).runtimeType, FamilyScreen); router.go('/foo'); - matches = router.routerDelegate.matches; + matches = router.routerDelegate.matches.matches; expect(matches, hasLength(2)); expect(router.screenFor(matches[1]).runtimeType, Page2Screen); }); @@ -465,7 +465,7 @@ void main() { final GoRouter router = await _router(routes, tester); const String loc = '/FaMiLy/f2'; router.go(loc); - final List matches = router.routerDelegate.matches; + final List matches = router.routerDelegate.matches.matches; // NOTE: match the lower case, since subloc is canonicalized to match the // path case whereas the location can be any case; so long as the path @@ -488,7 +488,7 @@ void main() { final GoRouter router = await _router(routes, tester); router.go('/user'); - final List matches = router.routerDelegate.matches; + final List matches = router.routerDelegate.matches.matches; expect(matches, hasLength(1)); expect(router.screenFor(matches.first).runtimeType, DummyScreen); }); @@ -738,7 +738,7 @@ void main() { router.goNamed('person', params: {'fid': 'f2', 'pid': 'p1'}); - final List matches = router.routerDelegate.matches; + final List matches = router.routerDelegate.matches.matches; expect(router.screenFor(matches.last).runtimeType, PersonScreen); }); @@ -762,7 +762,7 @@ void main() { log.info('loc= $loc'); router.go(loc); - final List matches = router.routerDelegate.matches; + final List matches = router.routerDelegate.matches.matches; log.info('param1= ${matches.first.decodedParams['param1']}'); expect(router.screenFor(matches.first).runtimeType, DummyScreen); expect(matches.first.decodedParams['param1'], param1); @@ -787,7 +787,7 @@ void main() { queryParams: {'param1': param1}); router.go(loc); await tester.pump(); - final List matches = router.routerDelegate.matches; + final List matches = router.routerDelegate.matches.matches; expect(router.screenFor(matches.first).runtimeType, DummyScreen); expect(matches.first.queryParams['param1'], param1); }); @@ -952,11 +952,12 @@ void main() { ? '/' : null); - final List matches = router.routerDelegate.matches; + final List matches = router.routerDelegate.matches.matches; expect(matches, hasLength(1)); - expect(router.screenFor(matches.first).runtimeType, ErrorScreen); - expect((router.screenFor(matches.first) as ErrorScreen).ex, isNotNull); - log.info((router.screenFor(matches.first) as ErrorScreen).ex); + expect(router.screenFor(matches.first).runtimeType, TestErrorScreen); + expect( + (router.screenFor(matches.first) as TestErrorScreen).ex, isNotNull); + log.info((router.screenFor(matches.first) as TestErrorScreen).ex); }); testWidgets('route-level redirect loop', (WidgetTester tester) async { @@ -974,11 +975,12 @@ void main() { tester, ); - final List matches = router.routerDelegate.matches; + final List matches = router.routerDelegate.matches.matches; expect(matches, hasLength(1)); - expect(router.screenFor(matches.first).runtimeType, ErrorScreen); - expect((router.screenFor(matches.first) as ErrorScreen).ex, isNotNull); - log.info((router.screenFor(matches.first) as ErrorScreen).ex); + expect(router.screenFor(matches.first).runtimeType, TestErrorScreen); + expect( + (router.screenFor(matches.first) as TestErrorScreen).ex, isNotNull); + log.info((router.screenFor(matches.first) as TestErrorScreen).ex); }); testWidgets('mixed redirect loop', (WidgetTester tester) async { @@ -994,11 +996,12 @@ void main() { state.subloc == '/' ? '/login' : null, ); - final List matches = router.routerDelegate.matches; + final List matches = router.routerDelegate.matches.matches; expect(matches, hasLength(1)); - expect(router.screenFor(matches.first).runtimeType, ErrorScreen); - expect((router.screenFor(matches.first) as ErrorScreen).ex, isNotNull); - log.info((router.screenFor(matches.first) as ErrorScreen).ex); + expect(router.screenFor(matches.first).runtimeType, TestErrorScreen); + expect( + (router.screenFor(matches.first) as TestErrorScreen).ex, isNotNull); + log.info((router.screenFor(matches.first) as TestErrorScreen).ex); }); testWidgets('top-level redirect loop w/ query params', @@ -1013,11 +1016,12 @@ void main() { : null, ); - final List matches = router.routerDelegate.matches; + final List matches = router.routerDelegate.matches.matches; expect(matches, hasLength(1)); - expect(router.screenFor(matches.first).runtimeType, ErrorScreen); - expect((router.screenFor(matches.first) as ErrorScreen).ex, isNotNull); - log.info((router.screenFor(matches.first) as ErrorScreen).ex); + expect(router.screenFor(matches.first).runtimeType, TestErrorScreen); + expect( + (router.screenFor(matches.first) as TestErrorScreen).ex, isNotNull); + log.info((router.screenFor(matches.first) as TestErrorScreen).ex); }); testWidgets('expect null path/fullpath on top-level redirect', @@ -1072,7 +1076,7 @@ void main() { }, ); - final List matches = router.routerDelegate.matches; + final List matches = router.routerDelegate.matches.matches; expect(matches, hasLength(1)); expect(router.screenFor(matches.first).runtimeType, LoginScreen); }); @@ -1101,7 +1105,7 @@ void main() { initialLocation: loc, ); - final List matches = router.routerDelegate.matches; + final List matches = router.routerDelegate.matches.matches; expect(matches, hasLength(1)); expect(router.screenFor(matches.first).runtimeType, HomeScreen); }); @@ -1142,7 +1146,7 @@ void main() { initialLocation: '/family/f2/person/p1', ); - final List matches = router.routerDelegate.matches; + final List matches = router.routerDelegate.matches.matches; expect(matches.length, 3); expect(router.screenFor(matches.first).runtimeType, HomeScreen); expect(router.screenFor(matches[1]).runtimeType, FamilyScreen); @@ -1155,15 +1159,16 @@ void main() { final GoRouter router = await _router( [], tester, - redirect: (GoRouterState state) => '${state.location}+', + redirect: (GoRouterState state) => '/${state.location}+', redirectLimit: 10, ); - final List matches = router.routerDelegate.matches; + final List matches = router.routerDelegate.matches.matches; expect(matches, hasLength(1)); - expect(router.screenFor(matches.first).runtimeType, ErrorScreen); - expect((router.screenFor(matches.first) as ErrorScreen).ex, isNotNull); - log.info((router.screenFor(matches.first) as ErrorScreen).ex); + expect(router.screenFor(matches.first).runtimeType, TestErrorScreen); + expect( + (router.screenFor(matches.first) as TestErrorScreen).ex, isNotNull); + log.info((router.screenFor(matches.first) as TestErrorScreen).ex); }); }); @@ -1264,7 +1269,7 @@ void main() { for (final String fid in ['f2', 'F2']) { final String loc = '/family/$fid'; router.go(loc); - final List matches = router.routerDelegate.matches; + final List matches = router.routerDelegate.matches.matches; expect(router.location, loc); expect(matches, hasLength(1)); @@ -1292,7 +1297,7 @@ void main() { for (final String fid in ['f2', 'F2']) { final String loc = '/family?fid=$fid'; router.go(loc); - final List matches = router.routerDelegate.matches; + final List matches = router.routerDelegate.matches.matches; expect(router.location, loc); expect(matches, hasLength(1)); @@ -1318,7 +1323,7 @@ void main() { final String loc = '/page1/${Uri.encodeComponent(param1)}'; router.go(loc); - final List matches = router.routerDelegate.matches; + final List matches = router.routerDelegate.matches.matches; log.info('param1= ${matches.first.decodedParams['param1']}'); expect(router.screenFor(matches.first).runtimeType, DummyScreen); expect(matches.first.decodedParams['param1'], param1); @@ -1340,14 +1345,14 @@ void main() { final GoRouter router = await _router(routes, tester); router.go('/page1?param1=$param1'); - final List matches = router.routerDelegate.matches; + final List matches = router.routerDelegate.matches.matches; expect(router.screenFor(matches.first).runtimeType, DummyScreen); expect(matches.first.queryParams['param1'], param1); final String loc = '/page1?param1=${Uri.encodeQueryComponent(param1)}'; router.go(loc); - final List matches2 = router.routerDelegate.matches; + final List matches2 = router.routerDelegate.matches.matches; expect(router.screenFor(matches2[0]).runtimeType, DummyScreen); expect(matches2[0].queryParams['param1'], param1); }); @@ -1388,7 +1393,7 @@ void main() { tester, initialLocation: '/?id=0&id=1', ); - final List matches = router.routerDelegate.matches; + final List matches = router.routerDelegate.matches.matches; expect(matches, hasLength(1)); expect(matches.first.fullpath, '/'); expect(router.screenFor(matches.first).runtimeType, HomeScreen); @@ -1411,7 +1416,7 @@ void main() { router.go('/0?id=1'); await tester.pump(); - final List matches = router.routerDelegate.matches; + final List matches = router.routerDelegate.matches.matches; expect(matches, hasLength(1)); expect(matches.first.fullpath, '/:id'); expect(router.screenFor(matches.first).runtimeType, HomeScreen); @@ -1445,11 +1450,12 @@ void main() { router.push('/person?fid=f2&pid=p1'); await tester.pump(); final FamilyScreen page1 = - router.screenFor(router.routerDelegate.matches.first) as FamilyScreen; + router.screenFor(router.routerDelegate.matches.matches.first) + as FamilyScreen; expect(page1.fid, 'f2'); - final PersonScreen page2 = - router.screenFor(router.routerDelegate.matches[1]) as PersonScreen; + final PersonScreen page2 = router + .screenFor(router.routerDelegate.matches.matches[1]) as PersonScreen; expect(page2.fid, 'f2'); expect(page2.pid, 'p1'); }); @@ -1482,11 +1488,12 @@ void main() { router.push('/person', extra: {'fid': 'f2', 'pid': 'p1'}); await tester.pump(); final FamilyScreen page1 = - router.screenFor(router.routerDelegate.matches.first) as FamilyScreen; + router.screenFor(router.routerDelegate.matches.matches.first) + as FamilyScreen; expect(page1.fid, 'f2'); - final PersonScreen page2 = - router.screenFor(router.routerDelegate.matches[1]) as PersonScreen; + final PersonScreen page2 = router + .screenFor(router.routerDelegate.matches.matches[1]) as PersonScreen; expect(page2.fid, 'f2'); expect(page2.pid, 'p1'); }); @@ -1523,7 +1530,7 @@ void main() { router.push(loc); await tester.pump(); - final List matches = router.routerDelegate.matches; + final List matches = router.routerDelegate.matches.matches; expect(router.location, loc); expect(matches, hasLength(2)); @@ -1548,8 +1555,8 @@ void main() { group('stream', () { test('no stream emits', () async { // Act - final GoRouterRefreshStreamSpy notifyListener = - GoRouterRefreshStreamSpy( + final StreamListenableSpy notifyListener = + StreamListenableSpy( streamController.stream, ); @@ -1565,8 +1572,8 @@ void main() { final List toEmit = [1, 2, 3]; // Act - final GoRouterRefreshStreamSpy notifyListener = - GoRouterRefreshStreamSpy( + final StreamListenableSpy notifyListener = + StreamListenableSpy( streamController.stream, ); @@ -1788,16 +1795,16 @@ void main() { GoRoute(path: '/', builder: (_, __) => const DummyStatefulWidget()), GoRoute( path: '/error', - builder: (_, __) => const GoRouterErrorScreen(null), + builder: (_, __) => const ErrorScreen(null), ), ], navigatorBuilder: navigatorBuilder, ); final GoRouterDelegate delegate = router.routerDelegate; - delegate.builderWithNav( + delegate.builder.builderWithNav( DummyBuildContext(), - GoRouterState(router.routeInformationParser, + GoRouterState(router.routeConfiguration, location: '/foo', subloc: '/bar', name: 'baz'), const Navigator(), ); @@ -1814,7 +1821,7 @@ Future createGoRouter( GoRoute(path: '/', builder: (_, __) => const DummyStatefulWidget()), GoRoute( path: '/error', - builder: (_, __) => const GoRouterErrorScreen(null), + builder: (_, __) => const ErrorScreen(null), ), ], navigatorBuilder: navigatorBuilder, @@ -1935,8 +1942,8 @@ class GoRouterPopSpy extends GoRouter { } } -class GoRouterRefreshStreamSpy extends GoRouterRefreshStream { - GoRouterRefreshStreamSpy( +class StreamListenableSpy extends StreamListenable { + StreamListenableSpy( Stream stream, ) : notifyCount = 0, super(stream); @@ -1963,7 +1970,7 @@ Future _router( initialLocation: initialLocation, redirectLimit: redirectLimit, errorBuilder: (BuildContext context, GoRouterState state) => - ErrorScreen(state.error!), + TestErrorScreen(state.error!), debugLogDiagnostics: false, ); await tester.pumpWidget( @@ -1976,8 +1983,8 @@ Future _router( return goRouter; } -class ErrorScreen extends DummyScreen { - const ErrorScreen(this.ex, {Key? key}) : super(key: key); +class TestErrorScreen extends DummyScreen { + const TestErrorScreen(this.ex, {Key? key}) : super(key: key); final Exception ex; } @@ -2023,15 +2030,15 @@ class DummyScreen extends StatelessWidget { Widget _dummy(BuildContext context, GoRouterState state) => const DummyScreen(); extension on GoRouter { - Page _pageFor(GoRouteMatch match) { - final List matches = routerDelegate.matches; + Page _pageFor(RouteMatch match) { + final List matches = routerDelegate.matches.matches; final int i = matches.indexOf(match); final List> pages = - routerDelegate.getPages(DummyBuildContext(), matches).toList(); + routerDelegate.builder.getPages(DummyBuildContext(), matches).toList(); return pages[i]; } - Widget screenFor(GoRouteMatch match) => + Widget screenFor(RouteMatch match) => (_pageFor(match) as MaterialPage).child; } @@ -2074,9 +2081,7 @@ class DummyBuildContext implements BuildContext { throw UnimplementedError(); } - // @override - // TODO(dit): Remove ignore below when flutter 2.11.0-0.0.pre.724 becomes stable - // ignore:annotate_overrides + @override void dispatchNotification(Notification notification) { throw UnimplementedError(); } diff --git a/packages/go_router/test/error_screen_helpers.dart b/packages/go_router/test/helpers/error_screen_helpers.dart similarity index 98% rename from packages/go_router/test/error_screen_helpers.dart rename to packages/go_router/test/helpers/error_screen_helpers.dart index c33d3282aed..5f372180c2b 100644 --- a/packages/go_router/test/error_screen_helpers.dart +++ b/packages/go_router/test/helpers/error_screen_helpers.dart @@ -7,7 +7,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:go_router/go_router.dart'; -import 'go_router_test.dart'; +import '../go_router_test.dart'; WidgetTesterCallback testPageNotFound({required Widget widget}) { return (WidgetTester tester) async { diff --git a/packages/go_router/test/go_route_information_provider_test.dart b/packages/go_router/test/information_provider_test.dart similarity index 94% rename from packages/go_router/test/go_route_information_provider_test.dart rename to packages/go_router/test/information_provider_test.dart index fd9d4e79072..e6ce0945508 100644 --- a/packages/go_router/test/go_route_information_provider_test.dart +++ b/packages/go_router/test/information_provider_test.dart @@ -4,7 +4,7 @@ import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:go_router/src/go_route_information_provider.dart'; +import 'package:go_router/src/information_provider.dart'; const RouteInformation initialRoute = RouteInformation(location: '/'); const RouteInformation newRoute = RouteInformation(location: '/new'); diff --git a/packages/go_router/test/inherited_go_router_test.dart b/packages/go_router/test/inherited_test.dart similarity index 100% rename from packages/go_router/test/inherited_go_router_test.dart rename to packages/go_router/test/inherited_test.dart diff --git a/packages/go_router/test/go_router_material_test.dart b/packages/go_router/test/material_test.dart similarity index 92% rename from packages/go_router/test/go_router_material_test.dart rename to packages/go_router/test/material_test.dart index 8a0c595474f..15e4ee0829f 100644 --- a/packages/go_router/test/go_router_material_test.dart +++ b/packages/go_router/test/material_test.dart @@ -5,9 +5,9 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:go_router/src/go_router_material.dart'; +import 'package:go_router/src/pages/material.dart'; -import 'error_screen_helpers.dart'; +import 'helpers/error_screen_helpers.dart'; void main() { group('isMaterialApp', () { @@ -63,7 +63,7 @@ void main() { 'shows "page not found" by default', testPageNotFound( widget: const MaterialApp( - home: GoRouterMaterialErrorScreen(null), + home: MaterialErrorScreen(null), ), ), ); @@ -74,7 +74,7 @@ void main() { testPageShowsExceptionMessage( exception: exception, widget: MaterialApp( - home: GoRouterMaterialErrorScreen(exception), + home: MaterialErrorScreen(exception), ), ), ); @@ -84,7 +84,7 @@ void main() { testClickingTheButtonRedirectsToRoot( buttonFinder: find.byType(TextButton), widget: const MaterialApp( - home: GoRouterMaterialErrorScreen(null), + home: MaterialErrorScreen(null), ), ), ); diff --git a/packages/go_router/test/go_route_information_parser_test.dart b/packages/go_router/test/parser_test.dart similarity index 67% rename from packages/go_router/test/go_route_information_parser_test.dart rename to packages/go_router/test/parser_test.dart index 067da09d533..69853543f51 100644 --- a/packages/go_router/test/go_route_information_parser_test.dart +++ b/packages/go_router/test/parser_test.dart @@ -4,9 +4,10 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:go_router/go_router.dart'; -import 'package:go_router/src/go_route_information_parser.dart'; -import 'package:go_router/src/go_route_match.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/parser.dart'; void main() { test('GoRouteInformationParser can parse route', () async { @@ -22,14 +23,17 @@ void main() { ], ), ]; - final GoRouteInformationParser parser = GoRouteInformationParser( - routes: routes, - redirectLimit: 100, - topRedirect: (_) => null, + final GoRouterInformationParser parser = GoRouterInformationParser( + configuration: RouteConfiguration( + routes: routes, + redirectLimit: 100, + topRedirect: (_) => null, + ), ); - List matches = await parser + RouteMatchList matchesObj = await parser .parseRouteInformation(const RouteInformation(location: '/')); + List matches = matchesObj.matches; expect(matches.length, 1); expect(matches[0].queryParams.isEmpty, isTrue); expect(matches[0].extra, isNull); @@ -38,8 +42,9 @@ void main() { expect(matches[0].route, routes[0]); final Object extra = Object(); - matches = await parser.parseRouteInformation( + matchesObj = await parser.parseRouteInformation( RouteInformation(location: '/abc?def=ghi', state: extra)); + matches = matchesObj.matches; expect(matches.length, 2); expect(matches[0].queryParams.length, 1); expect(matches[0].queryParams['def'], 'ghi'); @@ -69,14 +74,17 @@ void main() { ], ), ]; - final GoRouteInformationParser parser = GoRouteInformationParser( - routes: routes, - redirectLimit: 100, - topRedirect: (_) => null, + final GoRouterInformationParser parser = GoRouterInformationParser( + configuration: RouteConfiguration( + routes: routes, + redirectLimit: 100, + topRedirect: (_) => null, + ), ); - final List matches = await parser + final RouteMatchList matchesObj = await parser .parseRouteInformation(const RouteInformation(location: '/def')); + final List matches = matchesObj.matches; expect(matches.length, 1); expect(matches[0].queryParams.isEmpty, isTrue); expect(matches[0].extra, isNull); @@ -99,14 +107,18 @@ void main() { ], ), ]; - final GoRouteInformationParser parser = GoRouteInformationParser( - routes: routes, - redirectLimit: 100, - topRedirect: (_) => null, + final GoRouterInformationParser parser = GoRouterInformationParser( + configuration: RouteConfiguration( + routes: routes, + redirectLimit: 100, + topRedirect: (_) => null, + ), ); - final List matches = await parser.parseRouteInformation( + final RouteMatchList matchesObj = await parser.parseRouteInformation( const RouteInformation(location: '/123/family/456')); + final List matches = matchesObj.matches; + expect(matches.length, 2); expect(matches[0].queryParams.isEmpty, isTrue); expect(matches[0].extra, isNull); @@ -122,7 +134,9 @@ void main() { expect(matches[1].encodedParams['fid'], '456'); }); - test('GoRouteInformationParser can do top level redirect', () async { + test( + 'GoRouteInformationParser processes top level redirect when there is no match', + () async { final List routes = [ GoRoute( path: '/', @@ -135,19 +149,23 @@ void main() { ], ), ]; - final GoRouteInformationParser parser = GoRouteInformationParser( - routes: routes, - redirectLimit: 100, - topRedirect: (GoRouterState state) { - if (state.location != '/123/family/345') { - return '/123/family/345'; - } - return null; - }, + final GoRouterInformationParser parser = GoRouterInformationParser( + configuration: RouteConfiguration( + routes: routes, + redirectLimit: 100, + topRedirect: (GoRouterState state) { + if (state.location != '/123/family/345') { + return '/123/family/345'; + } + return null; + }, + ), ); - final List matches = await parser + final RouteMatchList matchesObj = await parser .parseRouteInformation(const RouteInformation(location: '/random/uri')); + final List matches = matchesObj.matches; + expect(matches.length, 2); expect(matches[0].fullUriString, '/'); expect(matches[0].subloc, '/'); @@ -156,7 +174,9 @@ void main() { expect(matches[1].subloc, '/123/family/345'); }); - test('GoRouteInformationParser can do route level redirect', () async { + test( + 'GoRouteInformationParser can do route level redirect when there is a match', + () async { final List routes = [ GoRoute( path: '/', @@ -174,14 +194,18 @@ void main() { ], ), ]; - final GoRouteInformationParser parser = GoRouteInformationParser( - routes: routes, - redirectLimit: 100, - topRedirect: (_) => null, + final GoRouterInformationParser parser = GoRouterInformationParser( + configuration: RouteConfiguration( + routes: routes, + redirectLimit: 100, + topRedirect: (_) => null, + ), ); - final List matches = await parser + final RouteMatchList matchesObj = await parser .parseRouteInformation(const RouteInformation(location: '/redirect')); + final List matches = matchesObj.matches; + expect(matches.length, 2); expect(matches[0].fullUriString, '/'); expect(matches[0].subloc, '/'); @@ -198,10 +222,12 @@ void main() { builder: (_, __) => const Placeholder(), ), ]; - final GoRouteInformationParser parser = GoRouteInformationParser( - routes: routes, - redirectLimit: 100, - topRedirect: (_) => null, + final GoRouterInformationParser parser = GoRouterInformationParser( + configuration: RouteConfiguration( + routes: routes, + redirectLimit: 100, + topRedirect: (_) => null, + ), ); expect(() async { @@ -219,14 +245,18 @@ void main() { redirect: (GoRouterState state) => state.location, ), ]; - final GoRouteInformationParser parser = GoRouteInformationParser( - routes: routes, - redirectLimit: 5, - topRedirect: (_) => null, + final GoRouterInformationParser parser = GoRouterInformationParser( + configuration: RouteConfiguration( + routes: routes, + redirectLimit: 5, + topRedirect: (_) => null, + ), ); - final List matches = await parser - .parseRouteInformation(const RouteInformation(location: '/abc')); + final RouteMatchList matchesObj = await parser + .parseRouteInformation(const RouteInformation(location: '/abd')); + final List matches = matchesObj.matches; + expect(matches, hasLength(1)); expect(matches.first.error, isNotNull); }); diff --git a/packages/go_router/test/path_parser_test.dart b/packages/go_router/test/path_parser_test.dart index 5d5ff8934a0..ed102269dcf 100644 --- a/packages/go_router/test/path_parser_test.dart +++ b/packages/go_router/test/path_parser_test.dart @@ -3,7 +3,7 @@ // found in the LICENSE file. import 'package:flutter_test/flutter_test.dart'; -import 'package:go_router/src/path_parser.dart'; +import 'package:go_router/src/path_utils.dart'; void main() { test('patternToRegExp without path parameter', () async { From aaec44a36349e5f96174c285da70d8e731d373d6 Mon Sep 17 00:00:00 2001 From: John Ryan Date: Tue, 12 Jul 2022 10:39:29 -0700 Subject: [PATCH 02/35] format --- packages/go_router/lib/go_router.dart | 68 +++++++++---------- packages/go_router/lib/src/builder.dart | 8 +-- packages/go_router/lib/src/configuration.dart | 14 ++-- packages/go_router/lib/src/typedefs.dart | 30 ++++---- packages/go_router/test/delegate_test.dart | 3 +- packages/go_router/test/go_router_test.dart | 6 +- 6 files changed, 63 insertions(+), 66 deletions(-) diff --git a/packages/go_router/lib/go_router.dart b/packages/go_router/lib/go_router.dart index 2d52837fb30..dacca23c48a 100644 --- a/packages/go_router/lib/go_router.dart +++ b/packages/go_router/lib/go_router.dart @@ -86,10 +86,10 @@ class GoRouter extends ChangeNotifier with NavigatorObserver { // allowing the caller to wrap the navigator themselves builderWithNav: (BuildContext context, GoRouterState state, Navigator nav) => - InheritedGoRouter( - goRouter: this, - child: navigatorBuilder?.call(context, state, nav) ?? nav, - ), + InheritedGoRouter( + goRouter: this, + child: navigatorBuilder?.call(context, state, nav) ?? nav, + ), ); assert(() { log.info('setting initial location $initialLocation'); @@ -116,10 +116,10 @@ class GoRouter extends ChangeNotifier with NavigatorObserver { /// Get a location from route name and parameters. /// This is useful for redirecting to a named location. String namedLocation( - String name, { - Map params = const {}, - Map queryParams = const {}, - }) => + String name, { + Map params = const {}, + Map queryParams = const {}, + }) => routeInformationParser.configuration.namedLocation( name, params: params, @@ -141,11 +141,11 @@ class GoRouter extends ChangeNotifier with NavigatorObserver { /// `name='person', params={'fid': 'f2', 'pid': 'p1'}` /// Navigate to the named route. void goNamed( - String name, { - Map params = const {}, - Map queryParams = const {}, - Object? extra, - }) => + String name, { + Map params = const {}, + Map queryParams = const {}, + Object? extra, + }) => go( namedLocation(name, params: params, queryParams: queryParams), extra: extra, @@ -160,7 +160,7 @@ class GoRouter extends ChangeNotifier with NavigatorObserver { }()); routeInformationParser .parseRouteInformation( - DebugGoRouteInformation(location: location, state: extra)) + DebugGoRouteInformation(location: location, state: extra)) .then((RouteMatchList matches) { routerDelegate.push(matches.last); }); @@ -169,11 +169,11 @@ class GoRouter extends ChangeNotifier with NavigatorObserver { /// Push a named route onto the page stack w/ optional parameters, e.g. /// `name='person', params={'fid': 'f2', 'pid': 'p1'}` void pushNamed( - String name, { - Map params = const {}, - Map queryParams = const {}, - Object? extra, - }) => + String name, { + Map params = const {}, + Map queryParams = const {}, + Object? extra, + }) => push( namedLocation(name, params: params, queryParams: queryParams), extra: extra, @@ -207,7 +207,7 @@ class GoRouter extends ChangeNotifier with NavigatorObserver { /// Find the current GoRouter in the widget tree. static GoRouter of(BuildContext context) { final InheritedGoRouter? inherited = - context.dependOnInheritedWidgetOfExactType(); + context.dependOnInheritedWidgetOfExactType(); assert(inherited != null, 'No GoRouter found in context'); return inherited!.goRouter; } @@ -287,10 +287,10 @@ class InheritedGoRouter extends InheritedWidget { extension GoRouterHelper on BuildContext { /// Get a location from route name and parameters. String namedLocation( - String name, { - Map params = const {}, - Map queryParams = const {}, - }) => + String name, { + Map params = const {}, + Map queryParams = const {}, + }) => GoRouter.of(this) .namedLocation(name, params: params, queryParams: queryParams); @@ -300,11 +300,11 @@ extension GoRouterHelper on BuildContext { /// Navigate to a named route. void goNamed( - String name, { - Map params = const {}, - Map queryParams = const {}, - Object? extra, - }) => + String name, { + Map params = const {}, + Map queryParams = const {}, + Object? extra, + }) => GoRouter.of(this).goNamed( name, params: params, @@ -318,11 +318,11 @@ extension GoRouterHelper on BuildContext { /// Navigate to a named route onto the page stack. void pushNamed( - String name, { - Map params = const {}, - Map queryParams = const {}, - Object? extra, - }) => + String name, { + Map params = const {}, + Map queryParams = const {}, + Object? extra, + }) => GoRouter.of(this).pushNamed( name, params: params, diff --git a/packages/go_router/lib/src/builder.dart b/packages/go_router/lib/src/builder.dart index eb37dcad7e1..2c073a0d0d9 100644 --- a/packages/go_router/lib/src/builder.dart +++ b/packages/go_router/lib/src/builder.dart @@ -233,16 +233,16 @@ class RouteBuilder { return true; }()); _pageBuilderForAppType = pageBuilderForMaterialApp; - _errorBuilderForAppType = (BuildContext c, GoRouterState s) => - MaterialErrorScreen(s.error); + _errorBuilderForAppType = + (BuildContext c, GoRouterState s) => MaterialErrorScreen(s.error); } else if (elem != null && isCupertinoApp(elem)) { assert(() { log.info('CupertinoApp found'); return true; }()); _pageBuilderForAppType = pageBuilderForCupertinoApp; - _errorBuilderForAppType = (BuildContext c, GoRouterState s) => - CupertinoErrorScreen(s.error); + _errorBuilderForAppType = + (BuildContext c, GoRouterState s) => CupertinoErrorScreen(s.error); } else { assert(() { log.info('WidgetsApp found'); diff --git a/packages/go_router/lib/src/configuration.dart b/packages/go_router/lib/src/configuration.dart index 399a7aafcb4..4ed7a7ce40c 100644 --- a/packages/go_router/lib/src/configuration.dart +++ b/packages/go_router/lib/src/configuration.dart @@ -48,10 +48,10 @@ class RouteConfiguration { /// Looks up the url location by a [GoRoute]'s name String namedLocation( - String name, { - Map params = const {}, - Map queryParams = const {}, - }) { + String name, { + Map params = const {}, + Map queryParams = const {}, + }) { assert(() { log.info('getting location for name: ' '"$name"' @@ -67,7 +67,7 @@ class RouteConfiguration { patternToRegExp(path, paramNames); for (final String paramName in paramNames) { assert(params.containsKey(paramName), - 'missing param "$paramName" for $path'); + 'missing param "$paramName" for $path'); } // Check that there are no extra params @@ -123,8 +123,8 @@ class RouteConfiguration { if (route.name != null) { final String name = route.name!.toLowerCase(); assert( - !_nameToPath.containsKey(name), - 'duplication fullpaths for name ' + !_nameToPath.containsKey(name), + 'duplication fullpaths for name ' '"$name":${_nameToPath[name]}, $fullPath'); _nameToPath[name] = fullPath; } diff --git a/packages/go_router/lib/src/typedefs.dart b/packages/go_router/lib/src/typedefs.dart index ead151cf1e4..c65a1f1086d 100644 --- a/packages/go_router/lib/src/typedefs.dart +++ b/packages/go_router/lib/src/typedefs.dart @@ -8,29 +8,29 @@ import 'configuration.dart'; /// The signature of the widget builder callback for a matched GoRoute. typedef GoRouterWidgetBuilder = Widget Function( - BuildContext context, - GoRouterState state, - ); + BuildContext context, + GoRouterState state, +); /// The signature of the page builder callback for a matched GoRoute. typedef GoRouterPageBuilder = Page Function( - BuildContext context, - GoRouterState state, - ); + BuildContext context, + GoRouterState state, +); /// The signature of the navigatorBuilder callback. typedef GoRouterNavigatorBuilder = Widget Function( - BuildContext context, - GoRouterState state, - Widget child, - ); + BuildContext context, + GoRouterState state, + Widget child, +); /// Signature of a go router builder function with navigator. typedef GoRouterBuilderWithNav = Widget Function( - BuildContext context, - GoRouterState state, - Navigator navigator, - ); + BuildContext context, + GoRouterState state, + Navigator navigator, +); /// The signature of the redirect callback. -typedef GoRouterRedirect = String? Function(GoRouterState state); \ No newline at end of file +typedef GoRouterRedirect = String? Function(GoRouterState state); diff --git a/packages/go_router/test/delegate_test.dart b/packages/go_router/test/delegate_test.dart index 81493f00b62..6a574dea7e9 100644 --- a/packages/go_router/test/delegate_test.dart +++ b/packages/go_router/test/delegate_test.dart @@ -37,8 +37,7 @@ void main() { ..push('/error'); goRouter.routerDelegate.addListener(expectAsync0(() {})); - final RouteMatch last = - goRouter.routerDelegate.matches.matches.last; + final RouteMatch last = goRouter.routerDelegate.matches.matches.last; goRouter.routerDelegate.pop(); expect(goRouter.routerDelegate.matches.matches.length, 1); expect(goRouter.routerDelegate.matches.matches.contains(last), false); diff --git a/packages/go_router/test/go_router_test.dart b/packages/go_router/test/go_router_test.dart index e161fd483e9..efc21a4d9d1 100644 --- a/packages/go_router/test/go_router_test.dart +++ b/packages/go_router/test/go_router_test.dart @@ -1555,8 +1555,7 @@ void main() { group('stream', () { test('no stream emits', () async { // Act - final StreamListenableSpy notifyListener = - StreamListenableSpy( + final StreamListenableSpy notifyListener = StreamListenableSpy( streamController.stream, ); @@ -1572,8 +1571,7 @@ void main() { final List toEmit = [1, 2, 3]; // Act - final StreamListenableSpy notifyListener = - StreamListenableSpy( + final StreamListenableSpy notifyListener = StreamListenableSpy( streamController.stream, ); From 61fbc2d7e538e4251e25af12a8d5749c8d75fd72 Mon Sep 17 00:00:00 2001 From: John Ryan Date: Tue, 12 Jul 2022 10:44:48 -0700 Subject: [PATCH 03/35] Sort imports --- packages/go_router/lib/src/match.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/go_router/lib/src/match.dart b/packages/go_router/lib/src/match.dart index db7d186070c..4a0b70b6ad9 100644 --- a/packages/go_router/lib/src/match.dart +++ b/packages/go_router/lib/src/match.dart @@ -4,8 +4,8 @@ import 'package:flutter/foundation.dart'; -import 'route.dart'; import 'path_utils.dart'; +import 'route.dart'; /// Each GoRouteMatch instance represents an instance of a GoRoute for a /// specific portion of a location. From 632b0125fb6036aa7c3ec834be99c57f1d301783 Mon Sep 17 00:00:00 2001 From: John Ryan Date: Tue, 12 Jul 2022 11:23:50 -0700 Subject: [PATCH 04/35] Update changelog --- packages/go_router/CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/go_router/CHANGELOG.md b/packages/go_router/CHANGELOG.md index 463f23da4ff..d6e8ec7f9c0 100644 --- a/packages/go_router/CHANGELOG.md +++ b/packages/go_router/CHANGELOG.md @@ -1,3 +1,7 @@ +## 4.1.1 + +- Refactors internal classes and methods + ## 4.1.0 - Adds `bool canPop()` to `GoRouterDelegate`, `GoRouter` and `GoRouterHelper`. From 40e2c61e9cea2c89c70c8229913531203059a566 Mon Sep 17 00:00:00 2001 From: John Ryan Date: Wed, 13 Jul 2022 11:39:42 -0700 Subject: [PATCH 05/35] Address code review comments - Change name back to GoRouterRefreshStream - Update toString() methods for new naming - Make fields final - Add logging to parser - Add comments - add tests - Move function-scope to new library-scope _addRedirect function - import widgets instead of material where possible --- .../example/lib/router_stream_refresh.dart | 4 +- packages/go_router/lib/src/configuration.dart | 4 +- packages/go_router/lib/src/match.dart | 10 +-- .../lib/src/misc/refresh_stream.dart | 6 +- packages/go_router/lib/src/parser.dart | 14 +++- packages/go_router/lib/src/redirection.dart | 75 ++++++++----------- packages/go_router/lib/src/typedefs.dart | 2 +- packages/go_router/test/go_router_test.dart | 10 ++- ..._parser_test.dart => path_utils_test.dart} | 32 ++++++++ 9 files changed, 94 insertions(+), 63 deletions(-) rename packages/go_router/test/{path_parser_test.dart => path_utils_test.dart} (75%) diff --git a/packages/go_router/example/lib/router_stream_refresh.dart b/packages/go_router/example/lib/router_stream_refresh.dart index baa3d767417..c656d286842 100644 --- a/packages/go_router/example/lib/router_stream_refresh.dart +++ b/packages/go_router/example/lib/router_stream_refresh.dart @@ -84,8 +84,8 @@ class _AppState extends State { return null; }, // changes on the listenable will cause the router to refresh it's route - // TODO(johnpryan): Change type to Stream, remove GoRouterRefreshStream - refreshListenable: StreamListenable(loggedInState.stream), + // TODO(johnpryan): Deprecate GoRouterRefreshStream + refreshListenable: GoRouterRefreshStream(loggedInState.stream), ); super.initState(); } diff --git a/packages/go_router/lib/src/configuration.dart b/packages/go_router/lib/src/configuration.dart index 4ed7a7ce40c..726cc732711 100644 --- a/packages/go_router/lib/src/configuration.dart +++ b/packages/go_router/lib/src/configuration.dart @@ -12,7 +12,7 @@ export 'state.dart'; /// The route configuration for GoRouter, configured by the app class RouteConfiguration { - /// Constructs a GoRouterConfiguration + /// Constructs a RouterConfiguration RouteConfiguration({ required this.routes, required this.redirectLimit, @@ -86,7 +86,7 @@ class RouteConfiguration { @override String toString() { - return 'GoRouterConfiguration: $routes'; + return 'RouterConfiguration: $routes'; } String _debugKnownRoutes() { diff --git a/packages/go_router/lib/src/match.dart b/packages/go_router/lib/src/match.dart index 4a0b70b6ad9..dd3e1754c09 100644 --- a/packages/go_router/lib/src/match.dart +++ b/packages/go_router/lib/src/match.dart @@ -7,10 +7,10 @@ import 'package:flutter/foundation.dart'; import 'path_utils.dart'; import 'route.dart'; -/// Each GoRouteMatch instance represents an instance of a GoRoute for a -/// specific portion of a location. +/// Each RouteMatch instance represents an instance of a GoRoute for a specific +/// portion of a location. class RouteMatch { - /// Constructor for GoRouteMatch, each instance represents an instance of a + /// Constructor for RouteMatch, each instance represents an instance of a /// GoRoute for a specific portion of a location. RouteMatch({ required this.route, @@ -106,7 +106,7 @@ class RouteMatch { param.key: Uri.decodeComponent(param.value) }; - /// for use by the Router architecture as part of the GoRouteMatch + /// For use by the Router architecture as part of the RouteMatch @override - String toString() => 'GoRouteMatch($fullpath, $encodedParams)'; + String toString() => 'RouteMatch($fullpath, $encodedParams)'; } diff --git a/packages/go_router/lib/src/misc/refresh_stream.dart b/packages/go_router/lib/src/misc/refresh_stream.dart index 8262ef70be3..01acb43a6e4 100644 --- a/packages/go_router/lib/src/misc/refresh_stream.dart +++ b/packages/go_router/lib/src/misc/refresh_stream.dart @@ -18,12 +18,12 @@ import 'package:flutter/material.dart'; /// ); /// ``` /// {@end-tool} -class StreamListenable extends ChangeNotifier { - /// Creates a [StreamListenable]. +class GoRouterRefreshStream extends ChangeNotifier { + /// Creates a [GoRouterRefreshStream]. /// /// Every time the [stream] receives an event the [GoRouter] will refresh its /// current route. - StreamListenable(Stream stream) { + GoRouterRefreshStream(Stream stream) { notifyListeners(); _subscription = stream.asBroadcastStream().listen( (dynamic _) => notifyListeners(), diff --git a/packages/go_router/lib/src/parser.dart b/packages/go_router/lib/src/parser.dart index 48cf9de64cf..771a402494e 100644 --- a/packages/go_router/lib/src/parser.dart +++ b/packages/go_router/lib/src/parser.dart @@ -7,6 +7,7 @@ import 'package:flutter/widgets.dart'; import 'configuration.dart'; import 'information_provider.dart'; +import 'logging.dart'; import 'match.dart'; import 'matching.dart'; import 'redirection.dart'; @@ -21,13 +22,13 @@ class GoRouterInformationParser extends RouteInformationParser { redirector = redirect; /// The route configuration for the app. - RouteConfiguration configuration; + final RouteConfiguration configuration; /// The route matcher - RouteMatcher matcher; + final RouteMatcher matcher; /// The route redirector - RouteRedirector redirector; + final RouteRedirector redirector; /// A debug property to assert [GoRouteInformationProvider] is in use along /// with this parser. @@ -59,6 +60,8 @@ class GoRouterInformationParser extends RouteInformationParser { initialMatches = matcher.findMatch(routeInformation.location!, extra: routeInformation.state); } on MatcherError { + log.info('No initial matches: ${routeInformation.location}'); + // If there is a matching error for the initial location, we should still // try to process the top-level redirects. initialMatches = RouteMatchList.empty(); @@ -77,9 +80,14 @@ class GoRouterInformationParser extends RouteInformationParser { // synchronously and remove unwanted initial animations on deep-linking return SynchronousFuture(matches); } on RedirectionError catch (e) { + log.info('Redirection error: ${e.message}'); final Uri uri = e.location; return SynchronousFuture(_errorScreen(uri, e.message)); } on MatcherError catch (e) { + // The RouteRedirector uses the matcher to find the match, so a match + // exception can happen during redirection. For example, the redirector + // redirects from `/a` to `/b`, it needs to get the matches for `/b`. + log.info('Match error: ${e.message}'); final Uri uri = Uri.parse(e.location); return SynchronousFuture(_errorScreen(uri, e.message)); } diff --git a/packages/go_router/lib/src/redirection.dart b/packages/go_router/lib/src/redirection.dart index 9c35c2981ff..5346d2d1426 100644 --- a/packages/go_router/lib/src/redirection.dart +++ b/packages/go_router/lib/src/redirection.dart @@ -23,39 +23,6 @@ RouteMatchList redirect(RouteMatchList prevMatchList, // Store each redirect to detect loops final List redirects = [prevMatchList]; - // Adds the redirect to the list of redirects if it is valid. - bool redirected(RouteMatchList newRedirect) { - if (newRedirect == null) { - return false; - } - - // Verify that the redirect can be parsed and is not already - // in the list of redirects - assert(() { - if (redirects.contains(newRedirect)) { - throw RedirectionError( - 'redirect loop detected', - [...redirects, newRedirect], - prevMatchList.location); - } - if (redirects.length > configuration.redirectLimit) { - throw RedirectionError( - 'too many redirects', - [...redirects, newRedirect], - prevMatchList.location); - } - return true; - }()); - - redirects.add(newRedirect); - - assert(() { - log.info('redirecting to $newRedirect'); - return true; - }()); - return true; - } - // Keep looping until redirecting is done while (true) { final RouteMatchList currentMatches = redirects.last; @@ -79,11 +46,9 @@ RouteMatchList redirect(RouteMatchList prevMatchList, if (topRedirectLocation != null) { final RouteMatchList newMatch = matcher.findMatch(topRedirectLocation); - if (redirected(newMatch)) { - continue; - } else { - matches = newMatch; - } + _addRedirect(redirects, newMatch, prevMatchList.location, + configuration.redirectLimit); + continue; } else { matches = currentMatches; } @@ -123,11 +88,9 @@ RouteMatchList redirect(RouteMatchList prevMatchList, } final RouteMatchList newMatchList = matcher.findMatch(topRouteLocation); - if (redirected(newMatchList)) { - continue; - } - - break; + _addRedirect(redirects, newMatchList, prevMatchList.location, + configuration.redirectLimit); + continue; } return matches; } @@ -154,3 +117,29 @@ class RedirectionError extends Error implements UnsupportedError { (RouteMatchList routeMatches) => routeMatches.location.toString()), ].join(' => '); } + +/// Adds the redirect to [redirects] if it is valid. +/// Returns true if the redirect was processed. +void _addRedirect(List redirects, RouteMatchList newMatch, + Uri prevLocation, int redirectLimit) { + // Verify that the redirect can be parsed and is not already + // in the list of redirects + assert(() { + if (redirects.contains(newMatch)) { + throw RedirectionError('redirect loop detected', + [...redirects, newMatch], prevLocation); + } + if (redirects.length > redirectLimit) { + throw RedirectionError('too many redirects', + [...redirects, newMatch], prevLocation); + } + return true; + }()); + + redirects.add(newMatch); + + assert(() { + log.info('redirecting to $newMatch'); + return true; + }()); +} diff --git a/packages/go_router/lib/src/typedefs.dart b/packages/go_router/lib/src/typedefs.dart index c65a1f1086d..2f8789e6f72 100644 --- a/packages/go_router/lib/src/typedefs.dart +++ b/packages/go_router/lib/src/typedefs.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/material.dart'; +import 'package:flutter/widgets.dart'; import 'configuration.dart'; diff --git a/packages/go_router/test/go_router_test.dart b/packages/go_router/test/go_router_test.dart index efc21a4d9d1..e9cffc4e39c 100644 --- a/packages/go_router/test/go_router_test.dart +++ b/packages/go_router/test/go_router_test.dart @@ -1555,7 +1555,8 @@ void main() { group('stream', () { test('no stream emits', () async { // Act - final StreamListenableSpy notifyListener = StreamListenableSpy( + final GoRouterRefreshStreamSpy notifyListener = + GoRouterRefreshStreamSpy( streamController.stream, ); @@ -1571,7 +1572,8 @@ void main() { final List toEmit = [1, 2, 3]; // Act - final StreamListenableSpy notifyListener = StreamListenableSpy( + final GoRouterRefreshStreamSpy notifyListener = + GoRouterRefreshStreamSpy( streamController.stream, ); @@ -1940,8 +1942,8 @@ class GoRouterPopSpy extends GoRouter { } } -class StreamListenableSpy extends StreamListenable { - StreamListenableSpy( +class GoRouterRefreshStreamSpy extends GoRouterRefreshStream { + GoRouterRefreshStreamSpy( Stream stream, ) : notifyCount = 0, super(stream); diff --git a/packages/go_router/test/path_parser_test.dart b/packages/go_router/test/path_utils_test.dart similarity index 75% rename from packages/go_router/test/path_parser_test.dart rename to packages/go_router/test/path_utils_test.dart index ed102269dcf..5c519e0294e 100644 --- a/packages/go_router/test/path_parser_test.dart +++ b/packages/go_router/test/path_utils_test.dart @@ -72,4 +72,36 @@ void main() { expect(url, restoredUrl); }); + + test('concatenatePaths', () { + void _verify(String pathA, String pathB, String expected) { + final String result = concatenatePaths(pathA, pathB); + expect(result, expected); + } + + void _verifyThrows(String pathA, String pathB) { + expect( + () => concatenatePaths(pathA, pathB), throwsA(isA())); + } + + _verify('/a', 'b/c', '/a/b/c'); + _verify('/', 'b', '/b'); + _verifyThrows('/a', '/b'); + _verifyThrows('/a', '/'); + _verifyThrows('/', '/'); + _verifyThrows('/', ''); + _verifyThrows('', ''); + }); + + test('canonicalUri', () { + void _verify(String path, String expected) => + expect(canonicalUri(path), expected); + _verify('/a', '/a'); + _verify('/a/', '/a'); + _verify('/', '/'); + _verify('/a/b/', '/a/b'); + + expect(() => canonicalUri('::::'), throwsA(isA())); + expect(() => canonicalUri(''), throwsA(anything)); + }); } From 61da05d1dc6888a56c619d5df0e19f09908221f7 Mon Sep 17 00:00:00 2001 From: John Ryan Date: Thu, 14 Jul 2022 12:47:28 -0700 Subject: [PATCH 06/35] remove routing library --- packages/go_router/lib/go_router.dart | 4 +++- packages/go_router/lib/src/routing.dart | 7 ------- 2 files changed, 3 insertions(+), 8 deletions(-) delete mode 100644 packages/go_router/lib/src/routing.dart diff --git a/packages/go_router/lib/go_router.dart b/packages/go_router/lib/go_router.dart index dacca23c48a..d8130ec49f2 100644 --- a/packages/go_router/lib/go_router.dart +++ b/packages/go_router/lib/go_router.dart @@ -10,10 +10,12 @@ import 'package:flutter/rendering.dart'; import 'package:flutter/widgets.dart'; import 'src/configuration.dart'; +import 'src/delegate.dart'; +import 'src/information_provider.dart'; import 'src/logging.dart'; import 'src/matching.dart'; +import 'src/parser.dart'; import 'src/platform.dart'; -import 'src/routing.dart'; import 'src/typedefs.dart'; export 'src/configuration.dart' show GoRouterState, GoRoute; diff --git a/packages/go_router/lib/src/routing.dart b/packages/go_router/lib/src/routing.dart deleted file mode 100644 index 71dc60badf3..00000000000 --- a/packages/go_router/lib/src/routing.dart +++ /dev/null @@ -1,7 +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. - -export 'delegate.dart'; -export 'information_provider.dart'; -export 'parser.dart'; From 1c2577c537cc8fcea056c0577d85050923758c94 Mon Sep 17 00:00:00 2001 From: John Ryan Date: Thu, 14 Jul 2022 12:55:53 -0700 Subject: [PATCH 07/35] Move classes in go_router.dart into separate libraries --- packages/go_router/lib/go_router.dart | 330 +----------------- .../go_router/lib/src/misc/error_screen.dart | 3 +- .../go_router/lib/src/misc/extensions.dart | 59 ++++ .../lib/src/misc/inherited_router.dart | 34 ++ .../go_router/lib/src/pages/cupertino.dart | 2 +- .../go_router/lib/src/pages/material.dart | 3 +- packages/go_router/lib/src/router.dart | 244 +++++++++++++ 7 files changed, 345 insertions(+), 330 deletions(-) create mode 100644 packages/go_router/lib/src/misc/extensions.dart create mode 100644 packages/go_router/lib/src/misc/inherited_router.dart create mode 100644 packages/go_router/lib/src/router.dart diff --git a/packages/go_router/lib/go_router.dart b/packages/go_router/lib/go_router.dart index d8130ec49f2..14536303dce 100644 --- a/packages/go_router/lib/go_router.dart +++ b/packages/go_router/lib/go_router.dart @@ -6,336 +6,12 @@ /// deep linking, data-driven routes and more library go_router; -import 'package:flutter/rendering.dart'; -import 'package:flutter/widgets.dart'; - -import 'src/configuration.dart'; -import 'src/delegate.dart'; -import 'src/information_provider.dart'; -import 'src/logging.dart'; -import 'src/matching.dart'; -import 'src/parser.dart'; -import 'src/platform.dart'; -import 'src/typedefs.dart'; - export 'src/configuration.dart' show GoRouterState, GoRoute; +export 'src/misc/extensions.dart'; +export 'src/misc/inherited_router.dart'; export 'src/misc/refresh_stream.dart'; export 'src/pages/custom_transition_page.dart'; export 'src/platform.dart' show UrlPathStrategy; +export 'src/router.dart'; export 'src/typed_routing.dart' show GoRouteData, TypedGoRoute; export 'src/typedefs.dart'; - -/// The top-level go router class. -/// -/// Create one of these to initialize your app's routing policy. -// ignore: prefer_mixin -class GoRouter extends ChangeNotifier with NavigatorObserver { - /// Default constructor to configure a GoRouter with a routes builder - /// and an error page builder. - GoRouter({ - required List routes, - // TODO(johnpryan): Change to a route, improve error API - GoRouterPageBuilder? errorPageBuilder, - GoRouterWidgetBuilder? errorBuilder, - GoRouterRedirect? redirect, - Listenable? refreshListenable, - int redirectLimit = 5, - bool routerNeglect = false, - String? initialLocation, - // TODO(johnpryan): Deprecate this parameter - UrlPathStrategy? urlPathStrategy, - List? observers, - bool debugLogDiagnostics = false, - // TODO(johnpryan): Deprecate this parameter - GoRouterNavigatorBuilder? navigatorBuilder, - String? restorationScopeId, - }) { - if (urlPathStrategy != null) { - setUrlPathStrategy(urlPathStrategy); - } - - setLogging(enabled: debugLogDiagnostics); - WidgetsFlutterBinding.ensureInitialized(); - - routeConfiguration = RouteConfiguration( - routes: routes, - topRedirect: redirect ?? (_) => null, - redirectLimit: redirectLimit, - ); - - routeConfiguration.validate(); - - routeInformationParser = GoRouterInformationParser( - configuration: routeConfiguration, - debugRequireGoRouteInformationProvider: true, - ); - routeInformationProvider = GoRouteInformationProvider( - initialRouteInformation: RouteInformation( - location: _effectiveInitialLocation(initialLocation)), - refreshListenable: refreshListenable); - - routerDelegate = GoRouterDelegate( - configuration: routeConfiguration, - errorPageBuilder: errorPageBuilder, - errorBuilder: errorBuilder, - routerNeglect: routerNeglect, - observers: [ - ...observers ?? [], - this - ], - restorationScopeId: restorationScopeId, - // wrap the returned Navigator to enable GoRouter.of(context).go() et al, - // allowing the caller to wrap the navigator themselves - builderWithNav: - (BuildContext context, GoRouterState state, Navigator nav) => - InheritedGoRouter( - goRouter: this, - child: navigatorBuilder?.call(context, state, nav) ?? nav, - ), - ); - assert(() { - log.info('setting initial location $initialLocation'); - return true; - }()); - } - - /// The route configuration for the app. - late final RouteConfiguration routeConfiguration; - - /// The route information parser used by the go router. - late final GoRouterInformationParser routeInformationParser; - - /// The router delegate used by the go router. - late final GoRouterDelegate routerDelegate; - - /// The route information provider used by the go router. - late final GoRouteInformationProvider routeInformationProvider; - - /// Get the current location. - String get location => - routerDelegate.currentConfiguration.location.toString(); - - /// Get a location from route name and parameters. - /// This is useful for redirecting to a named location. - String namedLocation( - String name, { - Map params = const {}, - Map queryParams = const {}, - }) => - routeInformationParser.configuration.namedLocation( - name, - params: params, - queryParams: queryParams, - ); - - /// Navigate to a URI location w/ optional query parameters, e.g. - /// `/family/f2/person/p1?color=blue` - void go(String location, {Object? extra}) { - assert(() { - log.info('going to $location'); - return true; - }()); - routeInformationProvider.value = - RouteInformation(location: location, state: extra); - } - - /// Navigate to a named route w/ optional parameters, e.g. - /// `name='person', params={'fid': 'f2', 'pid': 'p1'}` - /// Navigate to the named route. - void goNamed( - String name, { - Map params = const {}, - Map queryParams = const {}, - Object? extra, - }) => - go( - namedLocation(name, params: params, queryParams: queryParams), - extra: extra, - ); - - /// 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}) { - assert(() { - log.info('pushing $location'); - return true; - }()); - routeInformationParser - .parseRouteInformation( - DebugGoRouteInformation(location: location, state: extra)) - .then((RouteMatchList matches) { - routerDelegate.push(matches.last); - }); - } - - /// Push a named route onto the page stack w/ optional parameters, e.g. - /// `name='person', params={'fid': 'f2', 'pid': 'p1'}` - void pushNamed( - String name, { - Map params = const {}, - Map queryParams = const {}, - Object? extra, - }) => - push( - namedLocation(name, params: params, queryParams: queryParams), - extra: extra, - ); - - /// Returns `true` if there is more than 1 page on the stack. - bool canPop() => routerDelegate.canPop(); - - /// Pop the top page off the GoRouter's page stack. - void pop() { - assert(() { - log.info('popping $location'); - return true; - }()); - routerDelegate.pop(); - } - - /// Refresh the route. - void refresh() { - assert(() { - log.info('refreshing $location'); - return true; - }()); - routeInformationProvider.notifyListeners(); - } - - /// Set the app's URL path strategy (defaults to hash). call before runApp(). - static void setUrlPathStrategy(UrlPathStrategy strategy) => - setUrlPathStrategyImpl(strategy); - - /// Find the current GoRouter in the widget tree. - static GoRouter of(BuildContext context) { - final InheritedGoRouter? inherited = - context.dependOnInheritedWidgetOfExactType(); - assert(inherited != null, 'No GoRouter found in context'); - return inherited!.goRouter; - } - - /// The [Navigator] pushed `route`. - @override - void didPush(Route route, Route? previousRoute) => - notifyListeners(); - - /// The [Navigator] popped `route`. - @override - void didPop(Route route, Route? previousRoute) => - notifyListeners(); - - /// The [Navigator] removed `route`. - @override - void didRemove(Route route, Route? previousRoute) => - notifyListeners(); - - /// The [Navigator] replaced `oldRoute` with `newRoute`. - @override - void didReplace({Route? newRoute, Route? oldRoute}) => - notifyListeners(); - - @override - void dispose() { - routeInformationProvider.dispose(); - routerDelegate.dispose(); - super.dispose(); - } - - String _effectiveInitialLocation(String? initialLocation) { - final String platformDefault = - WidgetsBinding.instance.platformDispatcher.defaultRouteName; - if (initialLocation == null) { - return platformDefault; - } else if (platformDefault == '/') { - return initialLocation; - } else { - return platformDefault; - } - } -} - -/// GoRouter implementation of InheritedWidget. -/// -/// Used for to find the current GoRouter in the widget tree. This is useful -/// when routing from anywhere in your app. -class InheritedGoRouter extends InheritedWidget { - /// Default constructor for the inherited go router. - const InheritedGoRouter({ - required Widget child, - required this.goRouter, - Key? key, - }) : super(child: child, key: key); - - /// The [GoRouter] that is made available to the widget tree. - final GoRouter goRouter; - - /// Used by the Router architecture as part of the InheritedWidget. - @override - // ignore: prefer_expression_function_bodies - bool updateShouldNotify(covariant InheritedGoRouter oldWidget) { - // avoid rebuilding the widget tree if the router has not changed - return goRouter != oldWidget.goRouter; - } - - @override - void debugFillProperties(DiagnosticPropertiesBuilder properties) { - super.debugFillProperties(properties); - properties.add(DiagnosticsProperty('goRouter', goRouter)); - } -} - -/// Dart extension to add navigation function to a BuildContext object, e.g. -/// context.go('/'); -extension GoRouterHelper on BuildContext { - /// Get a location from route name and parameters. - String namedLocation( - String name, { - Map params = const {}, - Map queryParams = const {}, - }) => - GoRouter.of(this) - .namedLocation(name, params: params, queryParams: queryParams); - - /// Navigate to a location. - void go(String location, {Object? extra}) => - GoRouter.of(this).go(location, extra: extra); - - /// Navigate to a named route. - void goNamed( - String name, { - Map params = const {}, - Map queryParams = const {}, - Object? extra, - }) => - GoRouter.of(this).goNamed( - name, - params: params, - queryParams: queryParams, - extra: extra, - ); - - /// Push a location onto the page stack. - void push(String location, {Object? extra}) => - GoRouter.of(this).push(location, extra: extra); - - /// Navigate to a named route onto the page stack. - void pushNamed( - String name, { - Map params = const {}, - Map queryParams = const {}, - Object? extra, - }) => - GoRouter.of(this).pushNamed( - name, - params: params, - queryParams: queryParams, - extra: extra, - ); - - /// Returns `true` if there is more than 1 page on the stack. - bool canPop() => GoRouter.of(this).canPop(); - - /// Pop the top page off the Navigator's page stack by calling - /// [Navigator.pop]. - void pop() => GoRouter.of(this).pop(); -} diff --git a/packages/go_router/lib/src/misc/error_screen.dart b/packages/go_router/lib/src/misc/error_screen.dart index a965a87acb0..4572b977de4 100644 --- a/packages/go_router/lib/src/misc/error_screen.dart +++ b/packages/go_router/lib/src/misc/error_screen.dart @@ -3,7 +3,8 @@ // found in the LICENSE file. import 'package:flutter/widgets.dart'; -import '../../go_router.dart'; + +import 'extensions.dart'; /// Default error page implementation for WidgetsApp. class ErrorScreen extends StatelessWidget { diff --git a/packages/go_router/lib/src/misc/extensions.dart b/packages/go_router/lib/src/misc/extensions.dart new file mode 100644 index 00000000000..2098a404c20 --- /dev/null +++ b/packages/go_router/lib/src/misc/extensions.dart @@ -0,0 +1,59 @@ +import 'package:flutter/widgets.dart'; + +import '../router.dart'; + +/// Dart extension to add navigation function to a BuildContext object, e.g. +/// context.go('/'); +extension GoRouterHelper on BuildContext { + /// Get a location from route name and parameters. + String namedLocation( + String name, { + Map params = const {}, + Map queryParams = const {}, + }) => + GoRouter.of(this) + .namedLocation(name, params: params, queryParams: queryParams); + + /// Navigate to a location. + void go(String location, {Object? extra}) => + GoRouter.of(this).go(location, extra: extra); + + /// Navigate to a named route. + void goNamed( + String name, { + Map params = const {}, + Map queryParams = const {}, + Object? extra, + }) => + GoRouter.of(this).goNamed( + name, + params: params, + queryParams: queryParams, + extra: extra, + ); + + /// Push a location onto the page stack. + void push(String location, {Object? extra}) => + GoRouter.of(this).push(location, extra: extra); + + /// Navigate to a named route onto the page stack. + void pushNamed( + String name, { + Map params = const {}, + Map queryParams = const {}, + Object? extra, + }) => + GoRouter.of(this).pushNamed( + name, + params: params, + queryParams: queryParams, + extra: extra, + ); + + /// Returns `true` if there is more than 1 page on the stack. + bool canPop() => GoRouter.of(this).canPop(); + + /// Pop the top page off the Navigator's page stack by calling + /// [Navigator.pop]. + void pop() => GoRouter.of(this).pop(); +} diff --git a/packages/go_router/lib/src/misc/inherited_router.dart b/packages/go_router/lib/src/misc/inherited_router.dart new file mode 100644 index 00000000000..86d926d60bf --- /dev/null +++ b/packages/go_router/lib/src/misc/inherited_router.dart @@ -0,0 +1,34 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/widgets.dart'; + +import '../router.dart'; + +/// GoRouter implementation of InheritedWidget. +/// +/// Used for to find the current GoRouter in the widget tree. This is useful +/// when routing from anywhere in your app. +class InheritedGoRouter extends InheritedWidget { + /// Default constructor for the inherited go router. + const InheritedGoRouter({ + required Widget child, + required this.goRouter, + Key? key, + }) : super(child: child, key: key); + + /// The [GoRouter] that is made available to the widget tree. + final GoRouter goRouter; + + /// Used by the Router architecture as part of the InheritedWidget. + @override + // ignore: prefer_expression_function_bodies + bool updateShouldNotify(covariant InheritedGoRouter oldWidget) { + // avoid rebuilding the widget tree if the router has not changed + return goRouter != oldWidget.goRouter; + } + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties.add(DiagnosticsProperty('goRouter', goRouter)); + } +} diff --git a/packages/go_router/lib/src/pages/cupertino.dart b/packages/go_router/lib/src/pages/cupertino.dart index 9289d185bb9..6c9d1a97abd 100644 --- a/packages/go_router/lib/src/pages/cupertino.dart +++ b/packages/go_router/lib/src/pages/cupertino.dart @@ -5,7 +5,7 @@ // ignore_for_file: diagnostic_describe_all_properties import 'package:flutter/cupertino.dart'; -import '../../go_router.dart'; +import '../misc/extensions.dart'; /// Checks for CupertinoApp in the widget tree. bool isCupertinoApp(Element elem) => diff --git a/packages/go_router/lib/src/pages/material.dart b/packages/go_router/lib/src/pages/material.dart index 1d24c8c2daf..3eb656b4a32 100644 --- a/packages/go_router/lib/src/pages/material.dart +++ b/packages/go_router/lib/src/pages/material.dart @@ -5,7 +5,8 @@ // ignore_for_file: diagnostic_describe_all_properties import 'package:flutter/material.dart'; -import '../../go_router.dart'; + +import '../misc/extensions.dart'; /// Checks for MaterialApp in the widget tree. bool isMaterialApp(Element elem) => diff --git a/packages/go_router/lib/src/router.dart b/packages/go_router/lib/src/router.dart new file mode 100644 index 00000000000..aefff2921d8 --- /dev/null +++ b/packages/go_router/lib/src/router.dart @@ -0,0 +1,244 @@ +// 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 'configuration.dart'; +import 'delegate.dart'; +import 'information_provider.dart'; +import 'misc/inherited_router.dart'; +import 'logging.dart'; +import 'matching.dart'; +import 'parser.dart'; +import 'platform.dart'; +import 'typedefs.dart'; + +/// The top-level go router class. +/// +/// Create one of these to initialize your app's routing policy. +// ignore: prefer_mixin +class GoRouter extends ChangeNotifier with NavigatorObserver { + /// Default constructor to configure a GoRouter with a routes builder + /// and an error page builder. + GoRouter({ + required List routes, + // TODO(johnpryan): Change to a route, improve error API + GoRouterPageBuilder? errorPageBuilder, + GoRouterWidgetBuilder? errorBuilder, + GoRouterRedirect? redirect, + Listenable? refreshListenable, + int redirectLimit = 5, + bool routerNeglect = false, + String? initialLocation, + // TODO(johnpryan): Deprecate this parameter + UrlPathStrategy? urlPathStrategy, + List? observers, + bool debugLogDiagnostics = false, + // TODO(johnpryan): Deprecate this parameter + GoRouterNavigatorBuilder? navigatorBuilder, + String? restorationScopeId, + }) { + if (urlPathStrategy != null) { + setUrlPathStrategy(urlPathStrategy); + } + + setLogging(enabled: debugLogDiagnostics); + WidgetsFlutterBinding.ensureInitialized(); + + routeConfiguration = RouteConfiguration( + routes: routes, + topRedirect: redirect ?? (_) => null, + redirectLimit: redirectLimit, + ); + + routeConfiguration.validate(); + + routeInformationParser = GoRouterInformationParser( + configuration: routeConfiguration, + debugRequireGoRouteInformationProvider: true, + ); + routeInformationProvider = GoRouteInformationProvider( + initialRouteInformation: RouteInformation( + location: _effectiveInitialLocation(initialLocation)), + refreshListenable: refreshListenable); + + routerDelegate = GoRouterDelegate( + configuration: routeConfiguration, + errorPageBuilder: errorPageBuilder, + errorBuilder: errorBuilder, + routerNeglect: routerNeglect, + observers: [ + ...observers ?? [], + this + ], + restorationScopeId: restorationScopeId, + // wrap the returned Navigator to enable GoRouter.of(context).go() et al, + // allowing the caller to wrap the navigator themselves + builderWithNav: + (BuildContext context, GoRouterState state, Navigator nav) => + InheritedGoRouter( + goRouter: this, + child: navigatorBuilder?.call(context, state, nav) ?? nav, + ), + ); + assert(() { + log.info('setting initial location $initialLocation'); + return true; + }()); + } + + /// The route configuration for the app. + late final RouteConfiguration routeConfiguration; + + /// The route information parser used by the go router. + late final GoRouterInformationParser routeInformationParser; + + /// The router delegate used by the go router. + late final GoRouterDelegate routerDelegate; + + /// The route information provider used by the go router. + late final GoRouteInformationProvider routeInformationProvider; + + /// Get the current location. + String get location => + routerDelegate.currentConfiguration.location.toString(); + + /// Get a location from route name and parameters. + /// This is useful for redirecting to a named location. + String namedLocation( + String name, { + Map params = const {}, + Map queryParams = const {}, + }) => + routeInformationParser.configuration.namedLocation( + name, + params: params, + queryParams: queryParams, + ); + + /// Navigate to a URI location w/ optional query parameters, e.g. + /// `/family/f2/person/p1?color=blue` + void go(String location, {Object? extra}) { + assert(() { + log.info('going to $location'); + return true; + }()); + routeInformationProvider.value = + RouteInformation(location: location, state: extra); + } + + /// Navigate to a named route w/ optional parameters, e.g. + /// `name='person', params={'fid': 'f2', 'pid': 'p1'}` + /// Navigate to the named route. + void goNamed( + String name, { + Map params = const {}, + Map queryParams = const {}, + Object? extra, + }) => + go( + namedLocation(name, params: params, queryParams: queryParams), + extra: extra, + ); + + /// 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}) { + assert(() { + log.info('pushing $location'); + return true; + }()); + routeInformationParser + .parseRouteInformation( + DebugGoRouteInformation(location: location, state: extra)) + .then((RouteMatchList matches) { + routerDelegate.push(matches.last); + }); + } + + /// Push a named route onto the page stack w/ optional parameters, e.g. + /// `name='person', params={'fid': 'f2', 'pid': 'p1'}` + void pushNamed( + String name, { + Map params = const {}, + Map queryParams = const {}, + Object? extra, + }) => + push( + namedLocation(name, params: params, queryParams: queryParams), + extra: extra, + ); + + /// Returns `true` if there is more than 1 page on the stack. + bool canPop() => routerDelegate.canPop(); + + /// Pop the top page off the GoRouter's page stack. + void pop() { + assert(() { + log.info('popping $location'); + return true; + }()); + routerDelegate.pop(); + } + + /// Refresh the route. + void refresh() { + assert(() { + log.info('refreshing $location'); + return true; + }()); + routeInformationProvider.notifyListeners(); + } + + /// Set the app's URL path strategy (defaults to hash). call before runApp(). + static void setUrlPathStrategy(UrlPathStrategy strategy) => + setUrlPathStrategyImpl(strategy); + + /// Find the current GoRouter in the widget tree. + static GoRouter of(BuildContext context) { + final InheritedGoRouter? inherited = + context.dependOnInheritedWidgetOfExactType(); + assert(inherited != null, 'No GoRouter found in context'); + return inherited!.goRouter; + } + + /// The [Navigator] pushed `route`. + @override + void didPush(Route route, Route? previousRoute) => + notifyListeners(); + + /// The [Navigator] popped `route`. + @override + void didPop(Route route, Route? previousRoute) => + notifyListeners(); + + /// The [Navigator] removed `route`. + @override + void didRemove(Route route, Route? previousRoute) => + notifyListeners(); + + /// The [Navigator] replaced `oldRoute` with `newRoute`. + @override + void didReplace({Route? newRoute, Route? oldRoute}) => + notifyListeners(); + + @override + void dispose() { + routeInformationProvider.dispose(); + routerDelegate.dispose(); + super.dispose(); + } + + String _effectiveInitialLocation(String? initialLocation) { + final String platformDefault = + WidgetsBinding.instance.platformDispatcher.defaultRouteName; + if (initialLocation == null) { + return platformDefault; + } else if (platformDefault == '/') { + return initialLocation; + } else { + return platformDefault; + } + } +} From db91f560bcc39434a5320b8b3e6a98c6f2c83ad1 Mon Sep 17 00:00:00 2001 From: John Ryan Date: Thu, 14 Jul 2022 12:57:41 -0700 Subject: [PATCH 08/35] Move Configuration.validate() into constructor --- packages/go_router/lib/src/configuration.dart | 18 ++++++++---------- packages/go_router/lib/src/router.dart | 2 -- 2 files changed, 8 insertions(+), 12 deletions(-) diff --git a/packages/go_router/lib/src/configuration.dart b/packages/go_router/lib/src/configuration.dart index 726cc732711..9f4ea4557ee 100644 --- a/packages/go_router/lib/src/configuration.dart +++ b/packages/go_router/lib/src/configuration.dart @@ -19,10 +19,18 @@ class RouteConfiguration { required this.topRedirect, }) { _cacheNameToPath('', routes); + assert(() { _debugKnownRoutes(); return true; }()); + + for (final GoRoute route in routes) { + if (!route.path.startsWith('/')) { + throw RouteConfigurationError( + 'top-level path must start with "/": ${route.path}'); + } + } } /// List of top level routes used by the go router delegate @@ -36,16 +44,6 @@ class RouteConfiguration { final Map _nameToPath = {}; - /// Throws a [RouteConfigurationError] if this configuration is invalid - void validate() { - for (final GoRoute route in routes) { - if (!route.path.startsWith('/')) { - throw RouteConfigurationError( - 'top-level path must start with "/": ${route.path}'); - } - } - } - /// Looks up the url location by a [GoRoute]'s name String namedLocation( String name, { diff --git a/packages/go_router/lib/src/router.dart b/packages/go_router/lib/src/router.dart index aefff2921d8..8c57bf1dafb 100644 --- a/packages/go_router/lib/src/router.dart +++ b/packages/go_router/lib/src/router.dart @@ -52,8 +52,6 @@ class GoRouter extends ChangeNotifier with NavigatorObserver { redirectLimit: redirectLimit, ); - routeConfiguration.validate(); - routeInformationParser = GoRouterInformationParser( configuration: routeConfiguration, debugRequireGoRouteInformationProvider: true, From 1eaf6c3c804f12808ac7678036ae272de5e43ed7 Mon Sep 17 00:00:00 2001 From: John Ryan Date: Thu, 14 Jul 2022 13:00:05 -0700 Subject: [PATCH 09/35] Remove comment --- packages/go_router/lib/src/redirection.dart | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/go_router/lib/src/redirection.dart b/packages/go_router/lib/src/redirection.dart index 5346d2d1426..7ddf3507afc 100644 --- a/packages/go_router/lib/src/redirection.dart +++ b/packages/go_router/lib/src/redirection.dart @@ -119,7 +119,6 @@ class RedirectionError extends Error implements UnsupportedError { } /// Adds the redirect to [redirects] if it is valid. -/// Returns true if the redirect was processed. void _addRedirect(List redirects, RouteMatchList newMatch, Uri prevLocation, int redirectLimit) { // Verify that the redirect can be parsed and is not already From ea016655e6dea133ee17dbb899036d31bb010a27 Mon Sep 17 00:00:00 2001 From: John Ryan Date: Thu, 14 Jul 2022 13:04:17 -0700 Subject: [PATCH 10/35] use continue in redirect loop --- packages/go_router/lib/src/redirection.dart | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/packages/go_router/lib/src/redirection.dart b/packages/go_router/lib/src/redirection.dart index 7ddf3507afc..b63ce13a12c 100644 --- a/packages/go_router/lib/src/redirection.dart +++ b/packages/go_router/lib/src/redirection.dart @@ -42,17 +42,16 @@ RouteMatchList redirect(RouteMatchList prevMatchList, ), ); - // If the new location is null, keep the matches the same as before. if (topRedirectLocation != null) { final RouteMatchList newMatch = matcher.findMatch(topRedirectLocation); - _addRedirect(redirects, newMatch, prevMatchList.location, configuration.redirectLimit); continue; - } else { - matches = currentMatches; } + // If there's no top-level redirect, keep the matches the same as before. + matches = currentMatches; + // Merge new params to keep params from previously matched paths, e.g. // /users/:userId/book/:bookId provides userId and bookId to book/:bookId Map previouslyMatchedParams = {}; From d4c2ff96e000529b90ae93b8720d392a65d14cfd Mon Sep 17 00:00:00 2001 From: John Ryan Date: Thu, 14 Jul 2022 13:25:52 -0700 Subject: [PATCH 11/35] Fix comments --- .../go_router/example/lib/books/src/auth.dart | 2 +- .../lib/books/src/screens/scaffold.dart | 2 +- packages/go_router/example/lib/cupertino.dart | 2 +- .../go_router/example/lib/error_screen.dart | 2 +- .../example/lib/shared_scaffold.dart | 4 +- .../go_router/example/lib/transitions.dart | 2 +- packages/go_router/lib/go_router.dart | 2 +- packages/go_router/lib/src/builder.dart | 11 +++--- packages/go_router/lib/src/configuration.dart | 16 ++++---- packages/go_router/lib/src/delegate.dart | 6 +-- .../lib/src/information_provider.dart | 4 +- packages/go_router/lib/src/match.dart | 4 +- packages/go_router/lib/src/matching.dart | 38 +++++++++---------- packages/go_router/lib/src/parser.dart | 15 ++++---- .../src/platform/path_strategy_nonweb.dart | 2 +- .../lib/src/platform/path_strategy_web.dart | 4 +- packages/go_router/lib/src/redirection.dart | 2 +- packages/go_router/lib/src/route.dart | 4 +- 18 files changed, 62 insertions(+), 60 deletions(-) diff --git a/packages/go_router/example/lib/books/src/auth.dart b/packages/go_router/example/lib/books/src/auth.dart index 2b71e4c896b..2f5d8ce4e80 100644 --- a/packages/go_router/example/lib/books/src/auth.dart +++ b/packages/go_router/example/lib/books/src/auth.dart @@ -4,7 +4,7 @@ import 'package:flutter/widgets.dart'; -/// A mock authentication service +/// A mock authentication service. class BookstoreAuth extends ChangeNotifier { bool _signedIn = false; diff --git a/packages/go_router/example/lib/books/src/screens/scaffold.dart b/packages/go_router/example/lib/books/src/screens/scaffold.dart index a3f8134f63c..a5ff831eef3 100644 --- a/packages/go_router/example/lib/books/src/screens/scaffold.dart +++ b/packages/go_router/example/lib/books/src/screens/scaffold.dart @@ -7,7 +7,7 @@ import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; -/// The enum for scaffold tab +/// The enum for scaffold tab. enum ScaffoldTab { /// The books tab. books, diff --git a/packages/go_router/example/lib/cupertino.dart b/packages/go_router/example/lib/cupertino.dart index 3f4bd101840..6a8a704a6f3 100644 --- a/packages/go_router/example/lib/cupertino.dart +++ b/packages/go_router/example/lib/cupertino.dart @@ -7,7 +7,7 @@ import 'package:go_router/go_router.dart'; void main() => runApp(App()); -/// The main app +/// The main app. class App extends StatelessWidget { /// Creates an [App]. App({Key? key}) : super(key: key); diff --git a/packages/go_router/example/lib/error_screen.dart b/packages/go_router/example/lib/error_screen.dart index a578a832e92..b1c4fa9a354 100644 --- a/packages/go_router/example/lib/error_screen.dart +++ b/packages/go_router/example/lib/error_screen.dart @@ -85,7 +85,7 @@ class Page2Screen extends StatelessWidget { ); } -/// The screen of the error page +/// The screen of the error page. class ErrorScreen extends StatelessWidget { /// Creates an [ErrorScreen]. const ErrorScreen(this.error, {Key? key}) : super(key: key); diff --git a/packages/go_router/example/lib/shared_scaffold.dart b/packages/go_router/example/lib/shared_scaffold.dart index 3dc0c109b3a..fd2e72c4c8a 100644 --- a/packages/go_router/example/lib/shared_scaffold.dart +++ b/packages/go_router/example/lib/shared_scaffold.dart @@ -79,7 +79,7 @@ class SharedScaffold extends StatefulWidget { Key? key, }) : super(key: key); - /// The selected index + /// The selected index. final int selectedIndex; /// The body of the page. @@ -173,7 +173,7 @@ class Page2View extends StatelessWidget { /// The error scaffold. class ErrorScaffold extends StatelessWidget { - /// Creates an [ErrorScaffold] + /// Creates an [ErrorScaffold]. const ErrorScaffold({ required this.body, Key? key, diff --git a/packages/go_router/example/lib/transitions.dart b/packages/go_router/example/lib/transitions.dart index 08c8faa28dc..555e070acdd 100644 --- a/packages/go_router/example/lib/transitions.dart +++ b/packages/go_router/example/lib/transitions.dart @@ -137,7 +137,7 @@ class ExampleTransitionsScreen extends StatelessWidget { /// The color of the container. final Color color; - /// The transition kind + /// The transition kind. final String kind; @override diff --git a/packages/go_router/lib/go_router.dart b/packages/go_router/lib/go_router.dart index 14536303dce..5e2cfcaca52 100644 --- a/packages/go_router/lib/go_router.dart +++ b/packages/go_router/lib/go_router.dart @@ -3,7 +3,7 @@ // found in the LICENSE file. /// A declarative router for Flutter based on Navigation 2 supporting -/// deep linking, data-driven routes and more +/// deep linking, data-driven routes and more. library go_router; export 'src/configuration.dart' show GoRouterState, GoRoute; diff --git a/packages/go_router/lib/src/builder.dart b/packages/go_router/lib/src/builder.dart index 2c073a0d0d9..477a3413c86 100644 --- a/packages/go_router/lib/src/builder.dart +++ b/packages/go_router/lib/src/builder.dart @@ -15,9 +15,9 @@ import 'pages/material.dart'; import 'typed_routing.dart'; import 'typedefs.dart'; -/// Builds the top-level Navigator for GoRouter +/// Builds the top-level Navigator for GoRouter. class RouteBuilder { - /// GoRouteBuilder constructor + /// [RouteBuilder] constructor. RouteBuilder({ required this.configuration, required this.builderWithNav, @@ -43,8 +43,8 @@ class RouteBuilder { /// its history. final String? restorationScopeId; - /// NavigatorObserver used to receive change notifications when - /// navigation changes. + /// NavigatorObserver used to receive change notifications when navigation + /// changes. final List observers; /// Builds the top-level Navigator by invoking the build method on each @@ -142,7 +142,8 @@ class RouteBuilder { } /// Get the stack of sub-routes that matches the location and turn it into a - /// stack of pages, e.g. + /// stack of pages, for example: + /// /// routes: [ /// / /// family/:fid diff --git a/packages/go_router/lib/src/configuration.dart b/packages/go_router/lib/src/configuration.dart index 9f4ea4557ee..8def258b379 100644 --- a/packages/go_router/lib/src/configuration.dart +++ b/packages/go_router/lib/src/configuration.dart @@ -10,9 +10,9 @@ import 'typedefs.dart'; export 'route.dart'; export 'state.dart'; -/// The route configuration for GoRouter, configured by the app +/// The route configuration for GoRouter configured by the app. class RouteConfiguration { - /// Constructs a RouterConfiguration + /// Constructs a [RouteConfiguration]. RouteConfiguration({ required this.routes, required this.redirectLimit, @@ -33,10 +33,10 @@ class RouteConfiguration { } } - /// List of top level routes used by the go router delegate + /// The list of top level routes used by [GoRouterDelegate]. final List routes; - /// The limit for the number of consecutive redirects + /// The limit for the number of consecutive redirects. final int redirectLimit; /// Top level page redirect. @@ -44,7 +44,7 @@ class RouteConfiguration { final Map _nameToPath = {}; - /// Looks up the url location by a [GoRoute]'s name + /// Looks up the url location by a [GoRoute]'s name. String namedLocation( String name, { Map params = const {}, @@ -134,12 +134,12 @@ class RouteConfiguration { } } -/// Thrown when the [RouteConfiguration] is invalid +/// Thrown when the [RouteConfiguration] is invalid. class RouteConfigurationError extends Error { - /// RouteConfigurationError constructor + /// [RouteConfigurationError] constructor. RouteConfigurationError(this.message); - /// The error message + /// The error message. final String message; @override diff --git a/packages/go_router/lib/src/delegate.dart b/packages/go_router/lib/src/delegate.dart index 463c9d22ebb..b95f14747bc 100644 --- a/packages/go_router/lib/src/delegate.dart +++ b/packages/go_router/lib/src/delegate.dart @@ -13,11 +13,11 @@ import 'match.dart'; import 'matching.dart'; import 'typedefs.dart'; -/// GoRouter implementation of the RouterDelegate base class. +/// GoRouter implementation of [RouterDelegate]. class GoRouterDelegate extends RouterDelegate with PopNavigatorRouterDelegateMixin, ChangeNotifier { - /// Constructor for GoRouter's implementation of the - /// RouterDelegate base class. + /// Constructor for GoRouter's implementation of the RouterDelegate base + /// class. GoRouterDelegate({ required RouteConfiguration configuration, required GoRouterBuilderWithNav builderWithNav, diff --git a/packages/go_router/lib/src/information_provider.dart b/packages/go_router/lib/src/information_provider.dart index cf85765b546..55b8186528e 100644 --- a/packages/go_router/lib/src/information_provider.dart +++ b/packages/go_router/lib/src/information_provider.dart @@ -5,7 +5,7 @@ import 'package:flutter/services.dart'; import 'package:flutter/widgets.dart'; -/// The route information provider created by go_router +/// The [RouteInformationProvider] created by go_router. class GoRouteInformationProvider extends RouteInformationProvider with WidgetsBindingObserver, ChangeNotifier { /// Creates a [GoRouteInformationProvider]. @@ -112,7 +112,7 @@ class GoRouteInformationProvider extends RouteInformationProvider /// A debug class that is used for asserting the [GoRouteInformationProvider] is /// in use with the [RouteInformationParser]. class DebugGoRouteInformation extends RouteInformation { - /// Creates a [DebugGoRouteInformation] + /// Creates a [DebugGoRouteInformation]. DebugGoRouteInformation({String? location, Object? state}) : super(location: location, state: state); } diff --git a/packages/go_router/lib/src/match.dart b/packages/go_router/lib/src/match.dart index dd3e1754c09..d7f096e8a1b 100644 --- a/packages/go_router/lib/src/match.dart +++ b/packages/go_router/lib/src/match.dart @@ -10,8 +10,8 @@ import 'route.dart'; /// Each RouteMatch instance represents an instance of a GoRoute for a specific /// portion of a location. class RouteMatch { - /// Constructor for RouteMatch, each instance represents an instance of a - /// GoRoute for a specific portion of a location. + /// Constructor for [RouteMatch], each instance represents an instance of a + /// [GoRoute] for a specific portion of a location. RouteMatch({ required this.route, required this.subloc, diff --git a/packages/go_router/lib/src/matching.dart b/packages/go_router/lib/src/matching.dart index c94d9c9d2ec..67e46ecce47 100644 --- a/packages/go_router/lib/src/matching.dart +++ b/packages/go_router/lib/src/matching.dart @@ -6,15 +6,15 @@ import 'configuration.dart'; import 'match.dart'; import 'path_utils.dart'; -/// Converts a location into a list of [RouteMatch] objects +/// Converts a location into a list of [RouteMatch] objects. class RouteMatcher { - /// GoRouteMatcher constructor. + /// [RouteMatcher] constructor. RouteMatcher(this.configuration); - /// The route configuration + /// The route configuration. final RouteConfiguration configuration; - /// Finds the routes that matched the given URL + /// Finds the routes that matched the given URL. RouteMatchList findMatch(String location, {Object? extra}) { final String canonicalLocation = canonicalUri(location); final List matches = @@ -44,31 +44,31 @@ class RouteMatcher { /// The list of [RouteMatch] objects. class RouteMatchList { - /// GoRouteMatches constructor. + /// RouteMatchList constructor. RouteMatchList(this._matches); - /// Constructs an empty matches object + /// Constructs an empty matches object. factory RouteMatchList.empty() => RouteMatchList([]); final List _matches; - /// Returns true if there are no matches + /// Returns true if there are no matches. bool get isEmpty => _matches.isEmpty; - /// Returns true if there are matches + /// Returns true if there are matches. bool get isNotEmpty => _matches.isNotEmpty; - /// The original URL that was matched + /// The original URL that was matched. Uri get location => _matches.isEmpty ? Uri() : Uri.parse(_matches.last.fullUriString); - /// Pushes a match onto the list of matches + /// Pushes a match onto the list of matches. // TODO(johnpryan): deprecate this API when new route types are added void push(covariant RouteMatch match) { _matches.add(match); } - /// Removes the last match + /// Removes the last match. void pop() { _matches.removeLast(); assert( @@ -77,31 +77,31 @@ class RouteMatchList { ' there are no pages left to show'); } - /// Returns true if [pop] can safely be called + /// Returns true if [pop] can safely be called. bool canPop() { return _matches.length > 1; } - /// An optional object provided by the app during navigation + /// An optional object provided by the app during navigation. Object? get extra => _matches.last.extra; - /// The last matching route + /// The last matching route. RouteMatch get last => _matches.last; - /// The route matches + /// The route matches. List get matches => _matches; } -/// An error that occurred during matching +/// An error that occurred during matching. class MatcherError extends Error { - /// Constructs a MatcherError + /// Constructs a [MatcherError]. MatcherError(String message, this.location) : message = message + ': $location'; - /// The error message + /// The error message. final String message; - /// The location that failed to match + /// The location that failed to match. final String location; @override diff --git a/packages/go_router/lib/src/parser.dart b/packages/go_router/lib/src/parser.dart index 771a402494e..ad4034c648a 100644 --- a/packages/go_router/lib/src/parser.dart +++ b/packages/go_router/lib/src/parser.dart @@ -12,7 +12,8 @@ import 'match.dart'; import 'matching.dart'; import 'redirection.dart'; -/// Converts between incoming URLs and a [RouteMatchList] using [RouteMatcher]. Also performs redirection using [RouteRedirector] +/// Converts between incoming URLs and a [RouteMatchList] using [RouteMatcher]. +/// Also performs redirection using [RouteRedirector]. class GoRouterInformationParser extends RouteInformationParser { /// Creates a [GoRouterInformationParser]. GoRouterInformationParser({ @@ -24,10 +25,10 @@ class GoRouterInformationParser extends RouteInformationParser { /// The route configuration for the app. final RouteConfiguration configuration; - /// The route matcher + /// The route matcher. final RouteMatcher matcher; - /// The route redirector + /// The route redirector. final RouteRedirector redirector; /// A debug property to assert [GoRouteInformationProvider] is in use along @@ -39,7 +40,7 @@ class GoRouterInformationParser extends RouteInformationParser { /// Defaults to false. final bool debugRequireGoRouteInformationProvider; - /// for use by the Router architecture as part of the RouteInformationParser + /// Called by the [Router]. The @override Future parseRouteInformation( RouteInformation routeInformation, @@ -62,8 +63,8 @@ class GoRouterInformationParser extends RouteInformationParser { } on MatcherError { log.info('No initial matches: ${routeInformation.location}'); - // If there is a matching error for the initial location, we should still - // try to process the top-level redirects. + // If there is a matching error for the initial location, we should + // still try to process the top-level redirects. initialMatches = RouteMatchList.empty(); } final RouteMatchList matches = redirector( @@ -102,7 +103,7 @@ class GoRouterInformationParser extends RouteInformationParser { ); } - /// Creates a match that routes to the error page + /// Creates a match that routes to the error page. RouteMatchList _errorScreen(Uri uri, String errorMessage) { final Exception error = Exception(errorMessage); return RouteMatchList([ diff --git a/packages/go_router/lib/src/platform/path_strategy_nonweb.dart b/packages/go_router/lib/src/platform/path_strategy_nonweb.dart index 4bd9d8b2b4e..8caf922b680 100644 --- a/packages/go_router/lib/src/platform/path_strategy_nonweb.dart +++ b/packages/go_router/lib/src/platform/path_strategy_nonweb.dart @@ -4,7 +4,7 @@ import 'url_path_strategy.dart'; -/// no-op implementation of the URL path strategy for non-web target platforms +/// no-op implementation of the URL path strategy for non-web target platforms. // TODO(johnpryan): Remove this API void setUrlPathStrategyImpl(UrlPathStrategy strategy) { // no-op diff --git a/packages/go_router/lib/src/platform/path_strategy_web.dart b/packages/go_router/lib/src/platform/path_strategy_web.dart index ede76ec4350..c4dc30bdb1e 100644 --- a/packages/go_router/lib/src/platform/path_strategy_web.dart +++ b/packages/go_router/lib/src/platform/path_strategy_web.dart @@ -7,8 +7,8 @@ import 'package:flutter_web_plugins/flutter_web_plugins.dart'; import 'url_path_strategy.dart'; -/// forwarding implementation of the URL path strategy for the web target -/// platform +/// Forwarding implementation of the URL path strategy for the web target +/// platform. void setUrlPathStrategyImpl(UrlPathStrategy strategy) { setUrlStrategy(strategy == UrlPathStrategy.path ? PathUrlStrategy() diff --git a/packages/go_router/lib/src/redirection.dart b/packages/go_router/lib/src/redirection.dart index b63ce13a12c..1feef857b6e 100644 --- a/packages/go_router/lib/src/redirection.dart +++ b/packages/go_router/lib/src/redirection.dart @@ -96,7 +96,7 @@ RouteMatchList redirect(RouteMatchList prevMatchList, /// A configuration error detected while processing redirects. class RedirectionError extends Error implements UnsupportedError { - /// RedirectionError constructor + /// RedirectionError constructor. RedirectionError(this.message, this.matches, this.location); /// The matches that were found while processing redirects. diff --git a/packages/go_router/lib/src/route.dart b/packages/go_router/lib/src/route.dart index e82f0562350..d7533132ef2 100644 --- a/packages/go_router/lib/src/route.dart +++ b/packages/go_router/lib/src/route.dart @@ -11,8 +11,8 @@ import 'typedefs.dart'; /// A declarative mapping between a route path and a page builder. class GoRoute { - /// Default constructor used to create mapping between a - /// route path and a page builder. + /// Default constructor used to create mapping between a route path and a page + /// builder. GoRoute({ required this.path, this.name, From 199c35339400abf4aca57efc9ae39ed322a1870f Mon Sep 17 00:00:00 2001 From: John Ryan Date: Thu, 14 Jul 2022 13:26:36 -0700 Subject: [PATCH 12/35] Sort imports --- packages/go_router/lib/src/router.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/go_router/lib/src/router.dart b/packages/go_router/lib/src/router.dart index 8c57bf1dafb..78a03e298f4 100644 --- a/packages/go_router/lib/src/router.dart +++ b/packages/go_router/lib/src/router.dart @@ -7,9 +7,9 @@ import 'package:flutter/widgets.dart'; import 'configuration.dart'; import 'delegate.dart'; import 'information_provider.dart'; -import 'misc/inherited_router.dart'; import 'logging.dart'; import 'matching.dart'; +import 'misc/inherited_router.dart'; import 'parser.dart'; import 'platform.dart'; import 'typedefs.dart'; From b8a65af0b7f2af3f997b4daa1d70b1d6e8606a1a Mon Sep 17 00:00:00 2001 From: John Ryan Date: Thu, 14 Jul 2022 14:16:52 -0700 Subject: [PATCH 13/35] Fix logging in configuration --- packages/go_router/lib/src/configuration.dart | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/packages/go_router/lib/src/configuration.dart b/packages/go_router/lib/src/configuration.dart index 8def258b379..8cfd6a43ba9 100644 --- a/packages/go_router/lib/src/configuration.dart +++ b/packages/go_router/lib/src/configuration.dart @@ -21,7 +21,7 @@ class RouteConfiguration { _cacheNameToPath('', routes); assert(() { - _debugKnownRoutes(); + log.info(_debugKnownRoutes()); return true; }()); @@ -106,10 +106,7 @@ class RouteConfiguration { List routes, String parentFullpath, int depth, StringBuffer sb) { for (final GoRoute route in routes) { final String fullpath = concatenatePaths(parentFullpath, route.path); - assert(() { - sb.writeln(' => ${''.padLeft(depth * 2)}$fullpath'); - return true; - }()); + sb.writeln(' => ${''.padLeft(depth * 2)}$fullpath'); _debugFullPathsFor(route.routes, fullpath, depth + 1, sb); } } From 85fe41d4037d212058c89d73d90ce548ea1b741c Mon Sep 17 00:00:00 2001 From: John Ryan Date: Thu, 14 Jul 2022 14:17:00 -0700 Subject: [PATCH 14/35] add visibleForTesting annotation --- packages/go_router/lib/src/delegate.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/go_router/lib/src/delegate.dart b/packages/go_router/lib/src/delegate.dart index b95f14747bc..199160faa62 100644 --- a/packages/go_router/lib/src/delegate.dart +++ b/packages/go_router/lib/src/delegate.dart @@ -36,6 +36,7 @@ class GoRouterDelegate extends RouterDelegate ); /// Builds the top-level Navigator given a configuration and location. + @visibleForTesting final RouteBuilder builder; /// Set to true to disable creating history entries on the web. From e0bb4f43feb3b76e807e8d6c9a5bc1a99241bc6c Mon Sep 17 00:00:00 2001 From: John Ryan Date: Thu, 14 Jul 2022 14:25:50 -0700 Subject: [PATCH 15/35] Updates from merge with main --- packages/go_router/test/go_router_test.dart | 8 ++++---- packages/go_router/test/test_helpers.dart | 19 +++++++++---------- 2 files changed, 13 insertions(+), 14 deletions(-) diff --git a/packages/go_router/test/go_router_test.dart b/packages/go_router/test/go_router_test.dart index aa4137f4c96..15b23c27da6 100644 --- a/packages/go_router/test/go_router_test.dart +++ b/packages/go_router/test/go_router_test.dart @@ -9,9 +9,9 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:go_router/go_router.dart'; -import 'package:logging/logging.dart'; import 'package:go_router/src/delegate.dart'; import 'package:go_router/src/match.dart'; +import 'package:logging/logging.dart'; import 'test_helpers.dart'; @@ -32,7 +32,7 @@ void main() { ]; final GoRouter router = await createRouter(routes, tester); - final List matches = router.routerDelegate.matches; + final List matches = router.routerDelegate.matches.matches; expect(matches, hasLength(1)); expect(matches.first.fullpath, '/'); expect(router.screenFor(matches.first).runtimeType, HomeScreen); @@ -1367,7 +1367,7 @@ void main() { ), ], errorBuilder: (BuildContext context, GoRouterState state) => - ErrorScreen(state.error!), + TestErrorScreen(state.error!), initialLocation: '/0/1/2/0/1', ); expect(false, true); @@ -1795,7 +1795,7 @@ void main() { GoRoute(path: '/', builder: (_, __) => const DummyStatefulWidget()), GoRoute( path: '/error', - builder: (_, __) => const ErrorScreen(null), + builder: (_, __) => TestErrorScreen(TestFailure('exception')), ), ], navigatorBuilder: navigatorBuilder, diff --git a/packages/go_router/test/test_helpers.dart b/packages/go_router/test/test_helpers.dart index 8f3ca2643b0..0af81660602 100644 --- a/packages/go_router/test/test_helpers.dart +++ b/packages/go_router/test/test_helpers.dart @@ -10,9 +10,8 @@ import 'package:flutter/material.dart'; import 'package:flutter/src/foundation/diagnostics.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:go_router/go_router.dart'; -import 'package:go_router/src/go_route_match.dart'; -import 'package:go_router/src/go_router_error_page.dart'; -import 'package:go_router/src/typedefs.dart'; +import 'package:go_router/src/match.dart'; +import 'package:go_router/src/misc/error_screen.dart'; Future createGoRouter( WidgetTester tester, { @@ -24,7 +23,7 @@ Future createGoRouter( GoRoute(path: '/', builder: (_, __) => const DummyStatefulWidget()), GoRoute( path: '/error', - builder: (_, __) => const GoRouterErrorScreen(null), + builder: (_, __) => const ErrorScreen(null), ), ], navigatorBuilder: navigatorBuilder, @@ -186,8 +185,8 @@ Future createRouter( return goRouter; } -class ErrorScreen extends DummyScreen { - const ErrorScreen(this.ex, {Key? key}) : super(key: key); +class TestErrorScreen extends DummyScreen { + const TestErrorScreen(this.ex, {Key? key}) : super(key: key); final Exception ex; } @@ -233,15 +232,15 @@ class DummyScreen extends StatelessWidget { Widget dummy(BuildContext context, GoRouterState state) => const DummyScreen(); extension Extension on GoRouter { - Page _pageFor(GoRouteMatch match) { - final List matches = routerDelegate.matches; + Page _pageFor(RouteMatch match) { + final List matches = routerDelegate.matches.matches; final int i = matches.indexOf(match); final List> pages = - routerDelegate.getPages(DummyBuildContext(), matches).toList(); + routerDelegate.builder.getPages(DummyBuildContext(), matches).toList(); return pages[i]; } - Widget screenFor(GoRouteMatch match) => + Widget screenFor(RouteMatch match) => (_pageFor(match) as MaterialPage).child; } From 4c17bba6774ee4b2af37b62535699cca1e91a28e Mon Sep 17 00:00:00 2001 From: John Ryan Date: Fri, 15 Jul 2022 09:33:33 -0700 Subject: [PATCH 16/35] Format --- .../go_router/lib/src/misc/extensions.dart | 28 ++++++------- packages/go_router/lib/src/router.dart | 40 +++++++++---------- 2 files changed, 34 insertions(+), 34 deletions(-) diff --git a/packages/go_router/lib/src/misc/extensions.dart b/packages/go_router/lib/src/misc/extensions.dart index 2098a404c20..03515ac1b37 100644 --- a/packages/go_router/lib/src/misc/extensions.dart +++ b/packages/go_router/lib/src/misc/extensions.dart @@ -7,10 +7,10 @@ import '../router.dart'; extension GoRouterHelper on BuildContext { /// Get a location from route name and parameters. String namedLocation( - String name, { - Map params = const {}, - Map queryParams = const {}, - }) => + String name, { + Map params = const {}, + Map queryParams = const {}, + }) => GoRouter.of(this) .namedLocation(name, params: params, queryParams: queryParams); @@ -20,11 +20,11 @@ extension GoRouterHelper on BuildContext { /// Navigate to a named route. void goNamed( - String name, { - Map params = const {}, - Map queryParams = const {}, - Object? extra, - }) => + String name, { + Map params = const {}, + Map queryParams = const {}, + Object? extra, + }) => GoRouter.of(this).goNamed( name, params: params, @@ -38,11 +38,11 @@ extension GoRouterHelper on BuildContext { /// Navigate to a named route onto the page stack. void pushNamed( - String name, { - Map params = const {}, - Map queryParams = const {}, - Object? extra, - }) => + String name, { + Map params = const {}, + Map queryParams = const {}, + Object? extra, + }) => GoRouter.of(this).pushNamed( name, params: params, diff --git a/packages/go_router/lib/src/router.dart b/packages/go_router/lib/src/router.dart index 78a03e298f4..f5cae6a4c10 100644 --- a/packages/go_router/lib/src/router.dart +++ b/packages/go_router/lib/src/router.dart @@ -75,10 +75,10 @@ class GoRouter extends ChangeNotifier with NavigatorObserver { // allowing the caller to wrap the navigator themselves builderWithNav: (BuildContext context, GoRouterState state, Navigator nav) => - InheritedGoRouter( - goRouter: this, - child: navigatorBuilder?.call(context, state, nav) ?? nav, - ), + InheritedGoRouter( + goRouter: this, + child: navigatorBuilder?.call(context, state, nav) ?? nav, + ), ); assert(() { log.info('setting initial location $initialLocation'); @@ -105,10 +105,10 @@ class GoRouter extends ChangeNotifier with NavigatorObserver { /// Get a location from route name and parameters. /// This is useful for redirecting to a named location. String namedLocation( - String name, { - Map params = const {}, - Map queryParams = const {}, - }) => + String name, { + Map params = const {}, + Map queryParams = const {}, + }) => routeInformationParser.configuration.namedLocation( name, params: params, @@ -130,11 +130,11 @@ class GoRouter extends ChangeNotifier with NavigatorObserver { /// `name='person', params={'fid': 'f2', 'pid': 'p1'}` /// Navigate to the named route. void goNamed( - String name, { - Map params = const {}, - Map queryParams = const {}, - Object? extra, - }) => + String name, { + Map params = const {}, + Map queryParams = const {}, + Object? extra, + }) => go( namedLocation(name, params: params, queryParams: queryParams), extra: extra, @@ -149,7 +149,7 @@ class GoRouter extends ChangeNotifier with NavigatorObserver { }()); routeInformationParser .parseRouteInformation( - DebugGoRouteInformation(location: location, state: extra)) + DebugGoRouteInformation(location: location, state: extra)) .then((RouteMatchList matches) { routerDelegate.push(matches.last); }); @@ -158,11 +158,11 @@ class GoRouter extends ChangeNotifier with NavigatorObserver { /// Push a named route onto the page stack w/ optional parameters, e.g. /// `name='person', params={'fid': 'f2', 'pid': 'p1'}` void pushNamed( - String name, { - Map params = const {}, - Map queryParams = const {}, - Object? extra, - }) => + String name, { + Map params = const {}, + Map queryParams = const {}, + Object? extra, + }) => push( namedLocation(name, params: params, queryParams: queryParams), extra: extra, @@ -196,7 +196,7 @@ class GoRouter extends ChangeNotifier with NavigatorObserver { /// Find the current GoRouter in the widget tree. static GoRouter of(BuildContext context) { final InheritedGoRouter? inherited = - context.dependOnInheritedWidgetOfExactType(); + context.dependOnInheritedWidgetOfExactType(); assert(inherited != null, 'No GoRouter found in context'); return inherited!.goRouter; } From 84490149241e83b1cd20cd0864a19955f070d527 Mon Sep 17 00:00:00 2001 From: John Ryan Date: Fri, 15 Jul 2022 11:35:59 -0700 Subject: [PATCH 17/35] Add TODOs to make Router implementation classes private --- packages/go_router/lib/src/matching.dart | 2 +- packages/go_router/lib/src/parser.dart | 6 +-- packages/go_router/lib/src/router.dart | 63 +++++++++++++++--------- packages/go_router/test/parser_test.dart | 14 +++--- 4 files changed, 50 insertions(+), 35 deletions(-) diff --git a/packages/go_router/lib/src/matching.dart b/packages/go_router/lib/src/matching.dart index 67e46ecce47..6266e17123f 100644 --- a/packages/go_router/lib/src/matching.dart +++ b/packages/go_router/lib/src/matching.dart @@ -64,7 +64,7 @@ class RouteMatchList { /// Pushes a match onto the list of matches. // TODO(johnpryan): deprecate this API when new route types are added - void push(covariant RouteMatch match) { + void push(RouteMatch match) { _matches.add(match); } diff --git a/packages/go_router/lib/src/parser.dart b/packages/go_router/lib/src/parser.dart index ad4034c648a..6af2147459b 100644 --- a/packages/go_router/lib/src/parser.dart +++ b/packages/go_router/lib/src/parser.dart @@ -14,9 +14,9 @@ import 'redirection.dart'; /// Converts between incoming URLs and a [RouteMatchList] using [RouteMatcher]. /// Also performs redirection using [RouteRedirector]. -class GoRouterInformationParser extends RouteInformationParser { - /// Creates a [GoRouterInformationParser]. - GoRouterInformationParser({ +class GoRouteInformationParser extends RouteInformationParser { + /// Creates a [GoRouteInformationParser]. + GoRouteInformationParser({ required this.configuration, this.debugRequireGoRouteInformationProvider = false, }) : matcher = RouteMatcher(configuration), diff --git a/packages/go_router/lib/src/router.dart b/packages/go_router/lib/src/router.dart index f5cae6a4c10..468f5dfb2ef 100644 --- a/packages/go_router/lib/src/router.dart +++ b/packages/go_router/lib/src/router.dart @@ -46,23 +46,24 @@ class GoRouter extends ChangeNotifier with NavigatorObserver { setLogging(enabled: debugLogDiagnostics); WidgetsFlutterBinding.ensureInitialized(); - routeConfiguration = RouteConfiguration( + _routeConfiguration = RouteConfiguration( routes: routes, topRedirect: redirect ?? (_) => null, redirectLimit: redirectLimit, ); - routeInformationParser = GoRouterInformationParser( - configuration: routeConfiguration, + _routeInformationParser = GoRouteInformationParser( + configuration: _routeConfiguration, debugRequireGoRouteInformationProvider: true, ); - routeInformationProvider = GoRouteInformationProvider( + + _routeInformationProvider = GoRouteInformationProvider( initialRouteInformation: RouteInformation( location: _effectiveInitialLocation(initialLocation)), refreshListenable: refreshListenable); - routerDelegate = GoRouterDelegate( - configuration: routeConfiguration, + _routerDelegate = GoRouterDelegate( + configuration: _routeConfiguration, errorPageBuilder: errorPageBuilder, errorBuilder: errorBuilder, routerNeglect: routerNeglect, @@ -86,30 +87,44 @@ class GoRouter extends ChangeNotifier with NavigatorObserver { }()); } - /// The route configuration for the app. - late final RouteConfiguration routeConfiguration; + late final RouteConfiguration _routeConfiguration; + late final GoRouteInformationParser _routeInformationParser; + late final GoRouterDelegate _routerDelegate; + late final GoRouteInformationProvider _routeInformationProvider; + + /// The router delegate. Provide this to the MaterialApp or CupertinoApp's + /// `.router()` constructor + // TODO(johnpryan): change type to RouterDelegate + GoRouterDelegate get routerDelegate => _routerDelegate; - /// The route information parser used by the go router. - late final GoRouterInformationParser routeInformationParser; + /// The route information provider used by [GoRouter]. + // TODO(johnpryan): change type to RouteInformationProvider + GoRouteInformationProvider get routeInformationProvider => + _routeInformationProvider; - /// The router delegate used by the go router. - late final GoRouterDelegate routerDelegate; + /// The route information parser used by [GoRouter]. + // TODO(johnpryan): change type to RouteInformationParser + GoRouteInformationParser get routeInformationParser => + _routeInformationParser; - /// The route information provider used by the go router. - late final GoRouteInformationProvider routeInformationProvider; + /// The route configuration. Used for testing. + // TODO(johnpryan): Remove this, integration tests shouldn't need access + @visibleForTesting + RouteConfiguration get routeConfiguration => _routeConfiguration; /// Get the current location. String get location => - routerDelegate.currentConfiguration.location.toString(); + _routerDelegate.currentConfiguration.location.toString(); /// Get a location from route name and parameters. /// This is useful for redirecting to a named location. + // TODO(johnpryan): Deprecate this API String namedLocation( String name, { Map params = const {}, Map queryParams = const {}, }) => - routeInformationParser.configuration.namedLocation( + _routeInformationParser.configuration.namedLocation( name, params: params, queryParams: queryParams, @@ -122,7 +137,7 @@ class GoRouter extends ChangeNotifier with NavigatorObserver { log.info('going to $location'); return true; }()); - routeInformationProvider.value = + _routeInformationProvider.value = RouteInformation(location: location, state: extra); } @@ -147,11 +162,11 @@ class GoRouter extends ChangeNotifier with NavigatorObserver { log.info('pushing $location'); return true; }()); - routeInformationParser + _routeInformationParser .parseRouteInformation( DebugGoRouteInformation(location: location, state: extra)) .then((RouteMatchList matches) { - routerDelegate.push(matches.last); + _routerDelegate.push(matches.last); }); } @@ -169,7 +184,7 @@ class GoRouter extends ChangeNotifier with NavigatorObserver { ); /// Returns `true` if there is more than 1 page on the stack. - bool canPop() => routerDelegate.canPop(); + bool canPop() => _routerDelegate.canPop(); /// Pop the top page off the GoRouter's page stack. void pop() { @@ -177,7 +192,7 @@ class GoRouter extends ChangeNotifier with NavigatorObserver { log.info('popping $location'); return true; }()); - routerDelegate.pop(); + _routerDelegate.pop(); } /// Refresh the route. @@ -186,7 +201,7 @@ class GoRouter extends ChangeNotifier with NavigatorObserver { log.info('refreshing $location'); return true; }()); - routeInformationProvider.notifyListeners(); + _routeInformationProvider.notifyListeners(); } /// Set the app's URL path strategy (defaults to hash). call before runApp(). @@ -223,8 +238,8 @@ class GoRouter extends ChangeNotifier with NavigatorObserver { @override void dispose() { - routeInformationProvider.dispose(); - routerDelegate.dispose(); + _routeInformationProvider.dispose(); + _routerDelegate.dispose(); super.dispose(); } diff --git a/packages/go_router/test/parser_test.dart b/packages/go_router/test/parser_test.dart index 69853543f51..7e962940149 100644 --- a/packages/go_router/test/parser_test.dart +++ b/packages/go_router/test/parser_test.dart @@ -23,7 +23,7 @@ void main() { ], ), ]; - final GoRouterInformationParser parser = GoRouterInformationParser( + final GoRouteInformationParser parser = GoRouteInformationParser( configuration: RouteConfiguration( routes: routes, redirectLimit: 100, @@ -74,7 +74,7 @@ void main() { ], ), ]; - final GoRouterInformationParser parser = GoRouterInformationParser( + final GoRouteInformationParser parser = GoRouteInformationParser( configuration: RouteConfiguration( routes: routes, redirectLimit: 100, @@ -107,7 +107,7 @@ void main() { ], ), ]; - final GoRouterInformationParser parser = GoRouterInformationParser( + final GoRouteInformationParser parser = GoRouteInformationParser( configuration: RouteConfiguration( routes: routes, redirectLimit: 100, @@ -149,7 +149,7 @@ void main() { ], ), ]; - final GoRouterInformationParser parser = GoRouterInformationParser( + final GoRouteInformationParser parser = GoRouteInformationParser( configuration: RouteConfiguration( routes: routes, redirectLimit: 100, @@ -194,7 +194,7 @@ void main() { ], ), ]; - final GoRouterInformationParser parser = GoRouterInformationParser( + final GoRouteInformationParser parser = GoRouteInformationParser( configuration: RouteConfiguration( routes: routes, redirectLimit: 100, @@ -222,7 +222,7 @@ void main() { builder: (_, __) => const Placeholder(), ), ]; - final GoRouterInformationParser parser = GoRouterInformationParser( + final GoRouteInformationParser parser = GoRouteInformationParser( configuration: RouteConfiguration( routes: routes, redirectLimit: 100, @@ -245,7 +245,7 @@ void main() { redirect: (GoRouterState state) => state.location, ), ]; - final GoRouterInformationParser parser = GoRouterInformationParser( + final GoRouteInformationParser parser = GoRouteInformationParser( configuration: RouteConfiguration( routes: routes, redirectLimit: 5, From 9b40f1ef3c43192318ea2846db214b47f8178e88 Mon Sep 17 00:00:00 2001 From: John Ryan Date: Fri, 15 Jul 2022 13:01:18 -0700 Subject: [PATCH 18/35] Add copyright headers --- packages/go_router/lib/src/misc/extensions.dart | 4 ++++ packages/go_router/lib/src/misc/inherited_router.dart | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/packages/go_router/lib/src/misc/extensions.dart b/packages/go_router/lib/src/misc/extensions.dart index 03515ac1b37..f395f9c33c7 100644 --- a/packages/go_router/lib/src/misc/extensions.dart +++ b/packages/go_router/lib/src/misc/extensions.dart @@ -1,3 +1,7 @@ +// 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 '../router.dart'; diff --git a/packages/go_router/lib/src/misc/inherited_router.dart b/packages/go_router/lib/src/misc/inherited_router.dart index 86d926d60bf..3aec90220c5 100644 --- a/packages/go_router/lib/src/misc/inherited_router.dart +++ b/packages/go_router/lib/src/misc/inherited_router.dart @@ -1,3 +1,7 @@ +// 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/foundation.dart'; import 'package:flutter/widgets.dart'; From 0e14a04d05092a13534d8960ce5d11ec11fe33fa Mon Sep 17 00:00:00 2001 From: John Ryan Date: Fri, 15 Jul 2022 15:12:44 -0700 Subject: [PATCH 19/35] Fix tests --- packages/go_router/test/test_helpers.dart | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/go_router/test/test_helpers.dart b/packages/go_router/test/test_helpers.dart index 0af81660602..fe275a9491e 100644 --- a/packages/go_router/test/test_helpers.dart +++ b/packages/go_router/test/test_helpers.dart @@ -11,7 +11,6 @@ import 'package:flutter/src/foundation/diagnostics.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/misc/error_screen.dart'; Future createGoRouter( WidgetTester tester, { @@ -23,7 +22,7 @@ Future createGoRouter( GoRoute(path: '/', builder: (_, __) => const DummyStatefulWidget()), GoRoute( path: '/error', - builder: (_, __) => const ErrorScreen(null), + builder: (_, __) => TestErrorScreen(TestFailure('Exception')), ), ], navigatorBuilder: navigatorBuilder, @@ -172,7 +171,7 @@ Future createRouter( initialLocation: initialLocation, redirectLimit: redirectLimit, errorBuilder: (BuildContext context, GoRouterState state) => - ErrorScreen(state.error!), + TestErrorScreen(state.error!), debugLogDiagnostics: false, ); await tester.pumpWidget( From e3458e6e39aa4d482ac0f48a7ada944fc4bc2dc4 Mon Sep 17 00:00:00 2001 From: John Ryan Date: Wed, 20 Jul 2022 11:44:57 -0700 Subject: [PATCH 20/35] format --- packages/go_router/lib/src/go_route_information_parser.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/go_router/lib/src/go_route_information_parser.dart b/packages/go_router/lib/src/go_route_information_parser.dart index e69de29bb2d..8b137891791 100644 --- a/packages/go_router/lib/src/go_route_information_parser.dart +++ b/packages/go_router/lib/src/go_route_information_parser.dart @@ -0,0 +1 @@ + From 13bc2732a2858d1f827ce927b35bc03a012de93e Mon Sep 17 00:00:00 2001 From: John Ryan Date: Wed, 20 Jul 2022 11:45:50 -0700 Subject: [PATCH 21/35] fix comment --- packages/go_router/lib/src/builder.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/go_router/lib/src/builder.dart b/packages/go_router/lib/src/builder.dart index 477a3413c86..1dbf66a8cfa 100644 --- a/packages/go_router/lib/src/builder.dart +++ b/packages/go_router/lib/src/builder.dart @@ -115,8 +115,8 @@ class RouteBuilder { GoRouterState( configuration, location: location, - name: null, // no name available at the top level + name: null, // trim the query params off the subloc to match route.redirect subloc: uri.path, // pass along the query params 'cuz that's all we have right now From 2a350ad7b26993be736ce88bc7a39e0ddbaa9bb5 Mon Sep 17 00:00:00 2001 From: John Ryan Date: Thu, 21 Jul 2022 09:42:27 -0700 Subject: [PATCH 22/35] Update packages/go_router/lib/src/parser.dart MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Loïc Sharma <737941+loic-sharma@users.noreply.github.com> --- packages/go_router/lib/src/parser.dart | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/go_router/lib/src/parser.dart b/packages/go_router/lib/src/parser.dart index 6af2147459b..c3f11caf791 100644 --- a/packages/go_router/lib/src/parser.dart +++ b/packages/go_router/lib/src/parser.dart @@ -51,7 +51,8 @@ class GoRouteInformationParser extends RouteInformationParser { routeInformation is DebugGoRouteInformation, 'This GoRouteInformationParser needs to be used with ' 'GoRouteInformationProvider, did you forget to pass in ' - 'GoRouter.routeInformationProvider to the Router constructor?'); + 'GoRouter.routeInformationProvider to the Router constructor?', + ); } return true; }()); From fc82dbb4923e06b7e11de8f69169a8b128852864 Mon Sep 17 00:00:00 2001 From: John Ryan Date: Thu, 21 Jul 2022 09:42:50 -0700 Subject: [PATCH 23/35] add whitespace --- packages/go_router/lib/src/redirection.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/go_router/lib/src/redirection.dart b/packages/go_router/lib/src/redirection.dart index 1feef857b6e..f63b00cf906 100644 --- a/packages/go_router/lib/src/redirection.dart +++ b/packages/go_router/lib/src/redirection.dart @@ -111,6 +111,7 @@ class RedirectionError extends Error implements UnsupportedError { @override String toString() => super.toString() + + ' ' + [ ...matches.map( (RouteMatchList routeMatches) => routeMatches.location.toString()), From 86c89610c3c0a3730afb35383aa7bfad59547d5f Mon Sep 17 00:00:00 2001 From: John Ryan Date: Thu, 21 Jul 2022 09:43:59 -0700 Subject: [PATCH 24/35] format --- packages/go_router/lib/src/parser.dart | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/go_router/lib/src/parser.dart b/packages/go_router/lib/src/parser.dart index c3f11caf791..3f716e44c7b 100644 --- a/packages/go_router/lib/src/parser.dart +++ b/packages/go_router/lib/src/parser.dart @@ -48,10 +48,10 @@ class GoRouteInformationParser extends RouteInformationParser { assert(() { if (debugRequireGoRouteInformationProvider) { assert( - routeInformation is DebugGoRouteInformation, - 'This GoRouteInformationParser needs to be used with ' - 'GoRouteInformationProvider, did you forget to pass in ' - 'GoRouter.routeInformationProvider to the Router constructor?', + routeInformation is DebugGoRouteInformation, + 'This GoRouteInformationParser needs to be used with ' + 'GoRouteInformationProvider, did you forget to pass in ' + 'GoRouter.routeInformationProvider to the Router constructor?', ); } return true; From 7f8954c7a69ee245f0042cdfbec7f2efa31e003b Mon Sep 17 00:00:00 2001 From: John Ryan Date: Thu, 21 Jul 2022 09:52:09 -0700 Subject: [PATCH 25/35] Hide typedefs that weren't previously exported --- 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 5e2cfcaca52..4d0390172fa 100644 --- a/packages/go_router/lib/go_router.dart +++ b/packages/go_router/lib/go_router.dart @@ -14,4 +14,4 @@ export 'src/pages/custom_transition_page.dart'; export 'src/platform.dart' show UrlPathStrategy; export 'src/router.dart'; export 'src/typed_routing.dart' show GoRouteData, TypedGoRoute; -export 'src/typedefs.dart'; +export 'src/typedefs.dart' show GoRouterPageBuilder, GoRouterRedirect; From fdcf0ed83e594c73e0daea81bad1e32650e78b72 Mon Sep 17 00:00:00 2001 From: John Ryan Date: Thu, 21 Jul 2022 09:57:47 -0700 Subject: [PATCH 26/35] Delete empty file --- packages/go_router/lib/src/go_route_information_parser.dart | 1 - 1 file changed, 1 deletion(-) delete mode 100644 packages/go_router/lib/src/go_route_information_parser.dart diff --git a/packages/go_router/lib/src/go_route_information_parser.dart b/packages/go_router/lib/src/go_route_information_parser.dart deleted file mode 100644 index 8b137891791..00000000000 --- a/packages/go_router/lib/src/go_route_information_parser.dart +++ /dev/null @@ -1 +0,0 @@ - From 78e60a66b67735bb5c5dd167a0d99dc432a9f3ae Mon Sep 17 00:00:00 2001 From: John Ryan Date: Thu, 21 Jul 2022 10:26:29 -0700 Subject: [PATCH 27/35] add missing import --- packages/go_router/test/test_helpers.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/go_router/test/test_helpers.dart b/packages/go_router/test/test_helpers.dart index fe275a9491e..5361ceff76f 100644 --- a/packages/go_router/test/test_helpers.dart +++ b/packages/go_router/test/test_helpers.dart @@ -11,6 +11,7 @@ import 'package:flutter/src/foundation/diagnostics.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/typedefs.dart'; Future createGoRouter( WidgetTester tester, { From 26b4c12cd81a24ff38151f38ebe336bc27945493 Mon Sep 17 00:00:00 2001 From: John Ryan Date: Thu, 21 Jul 2022 10:40:17 -0700 Subject: [PATCH 28/35] Specify version 4.1.2 in pubspec.yaml --- packages/go_router/pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/go_router/pubspec.yaml b/packages/go_router/pubspec.yaml index 422a54c3253..72b55b161c2 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: 4.1.1 +version: 4.1.2 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 0c5436e5fe70f06f15e63033f7230ba7c309455d Mon Sep 17 00:00:00 2001 From: John Ryan Date: Thu, 21 Jul 2022 13:13:43 -0700 Subject: [PATCH 29/35] Update packages/go_router/lib/src/builder.dart Co-authored-by: chunhtai <47866232+chunhtai@users.noreply.github.com> --- packages/go_router/lib/src/builder.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/go_router/lib/src/builder.dart b/packages/go_router/lib/src/builder.dart index 1dbf66a8cfa..020a05f1d60 100644 --- a/packages/go_router/lib/src/builder.dart +++ b/packages/go_router/lib/src/builder.dart @@ -43,7 +43,7 @@ class RouteBuilder { /// its history. final String? restorationScopeId; - /// NavigatorObserver used to receive change notifications when navigation + /// NavigatorObserver used to receive notifications when navigating in between routes. /// changes. final List observers; From 348697b7f595cce9b35760158bd86572e2e6a547 Mon Sep 17 00:00:00 2001 From: John Ryan Date: Thu, 21 Jul 2022 13:20:18 -0700 Subject: [PATCH 30/35] Fix comment --- packages/go_router/lib/src/information_provider.dart | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/go_router/lib/src/information_provider.dart b/packages/go_router/lib/src/information_provider.dart index 55b8186528e..26bbe06c263 100644 --- a/packages/go_router/lib/src/information_provider.dart +++ b/packages/go_router/lib/src/information_provider.dart @@ -4,6 +4,7 @@ import 'package:flutter/services.dart'; import 'package:flutter/widgets.dart'; +import 'parser.dart'; /// The [RouteInformationProvider] created by go_router. class GoRouteInformationProvider extends RouteInformationProvider @@ -110,7 +111,7 @@ class GoRouteInformationProvider extends RouteInformationProvider } /// A debug class that is used for asserting the [GoRouteInformationProvider] is -/// in use with the [RouteInformationParser]. +/// in use with the [GoRouteInformationParser]. class DebugGoRouteInformation extends RouteInformation { /// Creates a [DebugGoRouteInformation]. DebugGoRouteInformation({String? location, Object? state}) From 77f2244ff5bc06372aa5b2189394a3798940fb71 Mon Sep 17 00:00:00 2001 From: John Ryan Date: Thu, 21 Jul 2022 13:26:37 -0700 Subject: [PATCH 31/35] Add isError and error getters to RouteMatchList --- packages/go_router/lib/src/builder.dart | 4 ++-- packages/go_router/lib/src/matching.dart | 6 ++++++ 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/packages/go_router/lib/src/builder.dart b/packages/go_router/lib/src/builder.dart index 020a05f1d60..28ef9b76a78 100644 --- a/packages/go_router/lib/src/builder.dart +++ b/packages/go_router/lib/src/builder.dart @@ -104,8 +104,8 @@ class RouteBuilder { // pass either the match error or the build error along to the navigator // builder, preferring the match error - if (matches.matches.length == 1 && matches.matches.first.error != null) { - error = matches.matches.first.error; + if (matches.isError) { + error = matches.error; } // wrap the returned Navigator to enable GoRouter.of(context).go() diff --git a/packages/go_router/lib/src/matching.dart b/packages/go_router/lib/src/matching.dart index 6266e17123f..0eefa82f60b 100644 --- a/packages/go_router/lib/src/matching.dart +++ b/packages/go_router/lib/src/matching.dart @@ -90,6 +90,12 @@ class RouteMatchList { /// The route matches. List get matches => _matches; + + /// Returns true if the current match intends to display an error screen. + 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; } /// An error that occurred during matching. From 820162d46c5d5464cfdc3bc2ed974c1bf29b50d5 Mon Sep 17 00:00:00 2001 From: John Ryan Date: Thu, 21 Jul 2022 16:56:20 -0700 Subject: [PATCH 32/35] Add issue links to TODO comments --- packages/go_router/lib/src/information_provider.dart | 1 + packages/go_router/lib/src/matching.dart | 1 - packages/go_router/lib/src/redirection.dart | 3 ++- packages/go_router/lib/src/router.dart | 7 ++++--- 4 files changed, 7 insertions(+), 5 deletions(-) diff --git a/packages/go_router/lib/src/information_provider.dart b/packages/go_router/lib/src/information_provider.dart index 26bbe06c263..0afeee561a2 100644 --- a/packages/go_router/lib/src/information_provider.dart +++ b/packages/go_router/lib/src/information_provider.dart @@ -33,6 +33,7 @@ class GoRouteInformationProvider extends RouteInformationProvider _valueInEngine.location == routeInformation.location); SystemNavigator.selectMultiEntryHistory(); // TODO(chunhtai): report extra to browser through state if possible + // See https://github.com/flutter/flutter/issues/108142 SystemNavigator.routeInformationUpdated( location: routeInformation.location!, replace: replace, diff --git a/packages/go_router/lib/src/matching.dart b/packages/go_router/lib/src/matching.dart index 0eefa82f60b..755aed67078 100644 --- a/packages/go_router/lib/src/matching.dart +++ b/packages/go_router/lib/src/matching.dart @@ -63,7 +63,6 @@ class RouteMatchList { _matches.isEmpty ? Uri() : Uri.parse(_matches.last.fullUriString); /// Pushes a match onto the list of matches. - // TODO(johnpryan): deprecate this API when new route types are added void push(RouteMatch match) { _matches.add(match); } diff --git a/packages/go_router/lib/src/redirection.dart b/packages/go_router/lib/src/redirection.dart index f63b00cf906..29b541d786c 100644 --- a/packages/go_router/lib/src/redirection.dart +++ b/packages/go_router/lib/src/redirection.dart @@ -8,7 +8,8 @@ import 'match.dart'; import 'matching.dart'; /// A GoRouter redirector function. -// TODO(johnpryan): make redirector async (#105808) +// TODO(johnpryan): make redirector async +// See https://github.com/flutter/flutter/issues/105808 typedef RouteRedirector = RouteMatchList Function(RouteMatchList matches, RouteConfiguration configuration, RouteMatcher matcher, {Object? extra}); diff --git a/packages/go_router/lib/src/router.dart b/packages/go_router/lib/src/router.dart index 468f5dfb2ef..155b0feff9a 100644 --- a/packages/go_router/lib/src/router.dart +++ b/packages/go_router/lib/src/router.dart @@ -24,6 +24,7 @@ class GoRouter extends ChangeNotifier with NavigatorObserver { GoRouter({ required List routes, // TODO(johnpryan): Change to a route, improve error API + // See https://github.com/flutter/flutter/issues/108144 GoRouterPageBuilder? errorPageBuilder, GoRouterWidgetBuilder? errorBuilder, GoRouterRedirect? redirect, @@ -32,10 +33,12 @@ class GoRouter extends ChangeNotifier with NavigatorObserver { bool routerNeglect = false, String? initialLocation, // TODO(johnpryan): Deprecate this parameter + // See https://github.com/flutter/flutter/issues/108132 UrlPathStrategy? urlPathStrategy, List? observers, bool debugLogDiagnostics = false, // TODO(johnpryan): Deprecate this parameter + // See https://github.com/flutter/flutter/issues/108145 GoRouterNavigatorBuilder? navigatorBuilder, String? restorationScopeId, }) { @@ -94,16 +97,13 @@ class GoRouter extends ChangeNotifier with NavigatorObserver { /// The router delegate. Provide this to the MaterialApp or CupertinoApp's /// `.router()` constructor - // TODO(johnpryan): change type to RouterDelegate GoRouterDelegate get routerDelegate => _routerDelegate; /// The route information provider used by [GoRouter]. - // TODO(johnpryan): change type to RouteInformationProvider GoRouteInformationProvider get routeInformationProvider => _routeInformationProvider; /// The route information parser used by [GoRouter]. - // TODO(johnpryan): change type to RouteInformationParser GoRouteInformationParser get routeInformationParser => _routeInformationParser; @@ -119,6 +119,7 @@ class GoRouter extends ChangeNotifier with NavigatorObserver { /// Get a location from route name and parameters. /// This is useful for redirecting to a named location. // TODO(johnpryan): Deprecate this API + // See https://github.com/flutter/flutter/issues/107729 String namedLocation( String name, { Map params = const {}, From 07f7ada1834413273e1ac2d5ebafefbb8fdf8d6d Mon Sep 17 00:00:00 2001 From: John Ryan Date: Fri, 22 Jul 2022 09:59:30 -0700 Subject: [PATCH 33/35] Add link to issue for TODO --- packages/go_router/example/lib/router_stream_refresh.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/go_router/example/lib/router_stream_refresh.dart b/packages/go_router/example/lib/router_stream_refresh.dart index c656d286842..7a3ecf4f58f 100644 --- a/packages/go_router/example/lib/router_stream_refresh.dart +++ b/packages/go_router/example/lib/router_stream_refresh.dart @@ -85,6 +85,7 @@ class _AppState extends State { }, // changes on the listenable will cause the router to refresh it's route // TODO(johnpryan): Deprecate GoRouterRefreshStream + // See https://github.com/flutter/flutter/issues/108128 refreshListenable: GoRouterRefreshStream(loggedInState.stream), ); super.initState(); From 5e282d359a9bd99e44d7477470d0f4813b9731d1 Mon Sep 17 00:00:00 2001 From: John Ryan Date: Fri, 22 Jul 2022 10:51:58 -0700 Subject: [PATCH 34/35] Re-apply code from #2306 due to merge conflicts --- packages/go_router/lib/src/delegate.dart | 9 ++++++ .../go_router/lib/src/misc/extensions.dart | 29 +++++++++++++++++++ packages/go_router/lib/src/router.dart | 4 +-- packages/go_router/test/delegate_test.dart | 22 +++++++------- 4 files changed, 51 insertions(+), 13 deletions(-) diff --git a/packages/go_router/lib/src/delegate.dart b/packages/go_router/lib/src/delegate.dart index 199160faa62..1af969c5376 100644 --- a/packages/go_router/lib/src/delegate.dart +++ b/packages/go_router/lib/src/delegate.dart @@ -63,6 +63,15 @@ class GoRouterDelegate extends RouterDelegate notifyListeners(); } + /// Replaces the top-most page of the page stack with the given one. + /// + /// See also: + /// * [push] which pushes the given location onto the page stack. + void replace(RouteMatch match) { + _matches.matches.last = match; + notifyListeners(); + } + /// For internal use; visible for testing only. @visibleForTesting RouteMatchList get matches => _matches; diff --git a/packages/go_router/lib/src/misc/extensions.dart b/packages/go_router/lib/src/misc/extensions.dart index f395f9c33c7..c818c2cd570 100644 --- a/packages/go_router/lib/src/misc/extensions.dart +++ b/packages/go_router/lib/src/misc/extensions.dart @@ -60,4 +60,33 @@ extension GoRouterHelper on BuildContext { /// Pop the top page off the Navigator's page stack by calling /// [Navigator.pop]. void pop() => GoRouter.of(this).pop(); + + /// Replaces the top-most page of the page stack with the given URL location + /// w/ optional query parameters, e.g. `/family/f2/person/p1?color=blue`. + /// + /// See also: + /// * [go] which navigates to the location. + /// * [push] which pushes the location onto the page stack. + void replace(String location, {Object? extra}) => + GoRouter.of(this).replace(location, extra: extra); + + /// Replaces the top-most page of the page stack with the named route w/ + /// optional parameters, e.g. `name='person', params={'fid': 'f2', 'pid': + /// 'p1'}`. + /// + /// See also: + /// * [goNamed] which navigates a named route. + /// * [pushNamed] which pushes a named route onto the page stack. + void replaceNamed( + String name, { + Map params = const {}, + Map queryParams = const {}, + Object? extra, + }) => + GoRouter.of(this).replaceNamed( + name, + params: params, + queryParams: queryParams, + extra: extra, + ); } diff --git a/packages/go_router/lib/src/router.dart b/packages/go_router/lib/src/router.dart index 65801366abd..c49c3563675 100644 --- a/packages/go_router/lib/src/router.dart +++ b/packages/go_router/lib/src/router.dart @@ -195,8 +195,8 @@ class GoRouter extends ChangeNotifier with NavigatorObserver { .parseRouteInformation( DebugGoRouteInformation(location: location, state: extra), ) - .then((List matches) { - routerDelegate.replace(matches.last); + .then((RouteMatchList matchList) { + routerDelegate.replace(matchList.matches.last); }); } diff --git a/packages/go_router/test/delegate_test.dart b/packages/go_router/test/delegate_test.dart index 39074012f1a..a85d53668a3 100644 --- a/packages/go_router/test/delegate_test.dart +++ b/packages/go_router/test/delegate_test.dart @@ -101,12 +101,12 @@ void main() { goRouter.push('/page-0'); goRouter.routerDelegate.addListener(expectAsync0(() {})); - final GoRouteMatch first = goRouter.routerDelegate.matches.first; - final GoRouteMatch last = goRouter.routerDelegate.matches.last; + final RouteMatch first = goRouter.routerDelegate.matches.matches.first; + final RouteMatch last = goRouter.routerDelegate.matches.last; goRouter.replace('/page-1'); - expect(goRouter.routerDelegate.matches.length, 2); + expect(goRouter.routerDelegate.matches.matches.length, 2); expect( - goRouter.routerDelegate.matches.first, + goRouter.routerDelegate.matches.matches.first, first, reason: 'The first match should still be in the list of matches', ); @@ -153,12 +153,12 @@ void main() { goRouter.pushNamed('page0'); goRouter.routerDelegate.addListener(expectAsync0(() {})); - final GoRouteMatch first = goRouter.routerDelegate.matches.first; - final GoRouteMatch last = goRouter.routerDelegate.matches.last; + final RouteMatch first = goRouter.routerDelegate.matches.matches.first; + final RouteMatch last = goRouter.routerDelegate.matches.last; goRouter.replaceNamed('page1'); - expect(goRouter.routerDelegate.matches.length, 2); + expect(goRouter.routerDelegate.matches.matches.length, 2); expect( - goRouter.routerDelegate.matches.first, + goRouter.routerDelegate.matches.matches.first, first, reason: 'The first match should still be in the list of matches', ); @@ -169,14 +169,14 @@ void main() { ); expect( goRouter.routerDelegate.matches.last, - isA() + isA() .having( - (GoRouteMatch match) => match.fullpath, + (RouteMatch match) => match.fullpath, 'match.fullpath', '/page-1', ) .having( - (GoRouteMatch match) => match.route.name, + (RouteMatch match) => match.route.name, 'match.route.name', 'page1', ), From 714e682f8ca378276b549a38bde91f3ba4daaba8 Mon Sep 17 00:00:00 2001 From: John Ryan Date: Fri, 22 Jul 2022 13:41:31 -0700 Subject: [PATCH 35/35] Add issue references --- packages/go_router/lib/src/state.dart | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/go_router/lib/src/state.dart b/packages/go_router/lib/src/state.dart index 9b30da1365f..e616dc4a68b 100644 --- a/packages/go_router/lib/src/state.dart +++ b/packages/go_router/lib/src/state.dart @@ -30,6 +30,7 @@ class GoRouterState { assert((path ?? '').isEmpty == (fullpath ?? '').isEmpty); // TODO(johnpryan): remove once namedLocation is removed from go_router. + // See https://github.com/flutter/flutter/issues/107729 final RouteConfiguration _configuration; /// The full location of the route, e.g. /family/f2/person/p1 @@ -65,6 +66,7 @@ class GoRouterState { /// Get a location from route name and parameters. /// This is useful for redirecting to a named location. // TODO(johnpryan): deprecate namedLocation API + // See https://github.com/flutter/flutter/issues/10772 String namedLocation( String name, { Map params = const {},