Revert "[go_router] Refactor RouterDelegate into functional pieces (#1653)" (#2183)

This reverts commit 2c02052ce8ba83edee76fef41a2475edab824c17.
This commit is contained in:
stuartmorgan
2022-06-01 15:29:56 -04:00
committed by GitHub
parent 2c02052ce8
commit d39ffb1c93
38 changed files with 964 additions and 1214 deletions

View File

@ -1,14 +1,10 @@
## 4.0.0
- Refactors go_router and introduces GoRouteInformationProvider. [Migration Doc](http://flutter.dev/go/go-router-v4-breaking-changes)
## 3.1.1
- Uses first match if there are more than one route to match. [ [#99833](https://github.com/flutter/flutter/issues/99833)
## 3.1.0
- Adds `GoRouteData` and `TypedGoRoute` to support `package:go_router_builder`.
- Added `GoRouteData` and `TypedGoRoute` to support `package:go_router_builder`.
## 3.0.7

View File

@ -15,7 +15,6 @@ class App extends StatelessWidget {
@override
Widget build(BuildContext context) => MaterialApp.router(
routeInformationProvider: _router.routeInformationProvider,
routeInformationParser: _router.routeInformationParser,
routerDelegate: _router.routerDelegate,
title: 'GoRouter Example',

View File

@ -26,7 +26,6 @@ class App extends StatelessWidget {
@override
Widget build(BuildContext context) => MaterialApp.router(
routeInformationProvider: _router.routeInformationProvider,
routeInformationParser: _router.routeInformationParser,
routerDelegate: _router.routerDelegate,
title: title,

View File

@ -31,7 +31,6 @@ class Bookstore extends StatelessWidget {
Widget build(BuildContext context) => BookstoreAuthScope(
notifier: _auth,
child: MaterialApp.router(
routeInformationProvider: _router.routeInformationProvider,
routerDelegate: _router.routerDelegate,
routeInformationParser: _router.routeInformationParser,
),

View File

@ -17,7 +17,6 @@ class App extends StatelessWidget {
@override
Widget build(BuildContext context) => CupertinoApp.router(
routeInformationProvider: _router.routeInformationProvider,
routeInformationParser: _router.routeInformationParser,
routerDelegate: _router.routerDelegate,
title: title,

View File

@ -17,7 +17,6 @@ class App extends StatelessWidget {
@override
Widget build(BuildContext context) => MaterialApp.router(
routeInformationProvider: _router.routeInformationProvider,
routeInformationParser: _router.routeInformationParser,
routerDelegate: _router.routerDelegate,
title: title,

View File

@ -27,7 +27,6 @@ class App extends StatelessWidget {
home: NoExtraParamOnWebScreen(),
)
: MaterialApp.router(
routeInformationProvider: _router.routeInformationProvider,
routeInformationParser: _router.routeInformationParser,
routerDelegate: _router.routerDelegate,
title: title,

View File

@ -17,7 +17,6 @@ class App extends StatelessWidget {
@override
Widget build(BuildContext context) => MaterialApp.router(
routeInformationProvider: _router.routeInformationProvider,
routeInformationParser: _router.routeInformationParser,
routerDelegate: _router.routerDelegate,
title: title,

View File

@ -55,7 +55,6 @@ class App extends StatelessWidget {
Widget build(BuildContext context) => ChangeNotifierProvider<AppState>.value(
value: _appState,
child: MaterialApp.router(
routeInformationProvider: _router.routeInformationProvider,
routeInformationParser: _router.routeInformationParser,
routerDelegate: _router.routerDelegate,
title: title,

View File

@ -17,7 +17,6 @@ class App extends StatelessWidget {
@override
Widget build(BuildContext context) => MaterialApp.router(
routeInformationProvider: _router.routeInformationProvider,
routeInformationParser: _router.routeInformationParser,
routerDelegate: _router.routerDelegate,
title: title,

View File

@ -24,7 +24,6 @@ class App extends StatelessWidget {
Widget build(BuildContext context) => ChangeNotifierProvider<LoginInfo>.value(
value: _loginInfo,
child: MaterialApp.router(
routeInformationProvider: _router.routeInformationProvider,
routeInformationParser: _router.routeInformationParser,
routerDelegate: _router.routerDelegate,
title: title,

View File

@ -22,7 +22,6 @@ class App extends StatelessWidget {
@override
Widget build(BuildContext context) => MaterialApp.router(
routeInformationProvider: _router.routeInformationProvider,
routeInformationParser: _router.routeInformationParser,
routerDelegate: _router.routerDelegate,
title: title,

View File

@ -18,7 +18,6 @@ class App extends StatelessWidget {
@override
Widget build(BuildContext context) => MaterialApp.router(
routeInformationProvider: _router.routeInformationProvider,
routeInformationParser: _router.routeInformationParser,
routerDelegate: _router.routerDelegate,
title: title,

View File

@ -19,7 +19,6 @@ class App extends StatelessWidget {
@override
Widget build(BuildContext context) => MaterialApp.router(
routeInformationProvider: _router.routeInformationProvider,
routeInformationParser: _router.routeInformationParser,
routerDelegate: _router.routerDelegate,
title: title,

View File

@ -17,7 +17,6 @@ class App extends StatelessWidget {
@override
Widget build(BuildContext context) => MaterialApp.router(
routeInformationProvider: _router.routeInformationProvider,
routeInformationParser: _router.routeInformationParser,
routerDelegate: _router.routerDelegate,
title: title,

View File

@ -25,7 +25,6 @@ class App extends StatelessWidget {
Widget build(BuildContext context) => ChangeNotifierProvider<LoginInfo>.value(
value: _loginInfo,
child: MaterialApp.router(
routeInformationProvider: _router.routeInformationProvider,
routeInformationParser: _router.routeInformationParser,
routerDelegate: _router.routerDelegate,
title: title,

View File

@ -25,7 +25,6 @@ class App extends StatelessWidget {
Widget build(BuildContext context) => ChangeNotifierProvider<LoginInfo>.value(
value: _loginInfo,
child: MaterialApp.router(
routeInformationProvider: _router.routeInformationProvider,
routeInformationParser: _router.routeInformationParser,
routerDelegate: _router.routerDelegate,
title: title,

View File

@ -17,7 +17,6 @@ class App extends StatelessWidget {
@override
Widget build(BuildContext context) => MaterialApp.router(
routeInformationProvider: _router.routeInformationProvider,
routeInformationParser: _router.routeInformationParser,
routerDelegate: _router.routerDelegate,
title: title,

View File

@ -94,7 +94,6 @@ class _AppState extends State<App> {
Widget build(BuildContext context) => Provider<LoggedInState>.value(
value: loggedInState,
child: MaterialApp.router(
routeInformationProvider: router.routeInformationProvider,
routeInformationParser: router.routeInformationParser,
routerDelegate: router.routerDelegate,
title: App.title,

View File

@ -19,7 +19,6 @@ class App extends StatelessWidget {
@override
Widget build(BuildContext context) => MaterialApp.router(
routeInformationProvider: _router.routeInformationProvider,
routeInformationParser: _router.routeInformationParser,
routerDelegate: _router.routerDelegate,
title: title,

View File

@ -32,7 +32,6 @@ class _AppState extends State<App> with RestorationMixin {
@override
Widget build(BuildContext context) => MaterialApp.router(
routeInformationProvider: _router.routeInformationProvider,
routeInformationParser: _router.routeInformationParser,
routerDelegate: _router.routerDelegate,
title: App.title,

View File

@ -19,7 +19,6 @@ class App extends StatelessWidget {
@override
Widget build(BuildContext context) => MaterialApp.router(
routeInformationProvider: _router.routeInformationProvider,
routeInformationParser: _router.routeInformationParser,
routerDelegate: _router.routerDelegate,
title: title,

View File

@ -17,7 +17,6 @@ class App extends StatelessWidget {
@override
Widget build(BuildContext context) => MaterialApp.router(
routeInformationProvider: _router.routeInformationProvider,
routeInformationParser: _router.routeInformationParser,
routerDelegate: _router.routerDelegate,
title: title,

View File

@ -25,7 +25,6 @@ class App extends StatelessWidget {
@override
Widget build(BuildContext context) => MaterialApp.router(
routeInformationProvider: _router.routeInformationProvider,
routeInformationParser: _router.routeInformationParser,
routerDelegate: _router.routerDelegate,
title: App.title,

View File

@ -20,7 +20,6 @@ class App extends StatelessWidget {
@override
Widget build(BuildContext context) => MaterialApp.router(
routeInformationProvider: _router.routeInformationProvider,
routeInformationParser: _router.routeInformationParser,
routerDelegate: _router.routerDelegate,
title: title,

View File

@ -21,7 +21,6 @@ class App extends StatelessWidget {
@override
Widget build(BuildContext context) => WidgetsApp.router(
routeInformationProvider: _router.routeInformationProvider,
routeInformationParser: _router.routeInformationParser,
routerDelegate: _router.routerDelegate,
title: title,

View File

@ -4,435 +4,20 @@
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<List<GoRouteMatch>> {
/// 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<GoRoute> 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 in not in use.
///
/// Defaults to false.
final bool debugRequireGoRouteInformationProvider;
final Map<String, String> _nameToPath = <String, String>{};
void _cacheNameToPath(String parentFullPath, List<GoRoute> 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<String, String> params = const <String, String>{},
Map<String, String> queryParams = const <String, String>{},
}) {
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 presented.
final List<String> paramNames = <String>[];
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<String, String> encodedParams = <String, String>{
for (final MapEntry<String, String> 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';
}
class GoRouteInformationParser extends RouteInformationParser<Uri> {
/// for use by the Router architecture as part of the RouteInformationParser
@override
Future<List<GoRouteMatch>> parseRouteInformation(
Future<Uri> parseRouteInformation(
RouteInformation routeInformation,
) {
assert(() {
if (debugRequireGoRouteInformationProvider) {
assert(
routeInformation is DebugGoRouteInformation,
'This GoRouteInformationParser needs to be used with '
'GoRouteInformationProvider, do you forget to pass in '
'GoRouter.routeinformationProvider to the Router constructor?');
}
return true;
}());
final List<GoRouteMatch> matches =
_getLocRouteMatchesWithRedirects(routeInformation);
// Use [SynchronousFuture] so that the initial url is processed
// synchronously and remove unwanted initial animations on deep-linking
return SynchronousFuture<List<GoRouteMatch>>(matches);
}
List<GoRouteMatch> _getLocRouteMatchesWithRedirects(
RouteInformation routeInformation) {
// start redirecting from the initial location
List<GoRouteMatch> matches;
final String location = routeInformation.location!;
try {
// watch redirects for loops
final List<String> redirects = <String>[_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: ${<String>[
...redirects,
redir
].join(' => ')}');
}
if (redirects.length > redirectLimit) {
throw _ParserError('too many redirects: ${<String>[
...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<String, String> previouslyMatchedParams = <String, String>{};
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>[
GoRouteMatch(
subloc: uri.path,
fullpath: uri.path,
encodedParams: <String, String>{},
queryParams: uri.queryParameters,
extra: null,
error: error,
route: GoRoute(
path: location,
pageBuilder: (BuildContext context, GoRouterState state) {
throw UnimplementedError();
}),
),
];
}
assert(matches.isNotEmpty);
return matches;
}
List<GoRouteMatch> _getLocRouteMatches(String location, Object? extra) {
final Uri uri = Uri.parse(location);
return _getLocRouteRecursively(
loc: uri.path,
restLoc: uri.path,
routes: routes,
parentFullpath: '',
parentSubloc: '',
queryParams: uri.queryParameters,
extra: extra,
);
}
static List<GoRouteMatch> _getLocRouteRecursively({
required String loc,
required String restLoc,
required String parentSubloc,
required List<GoRoute> routes,
required String parentFullpath,
required Map<String, String> queryParams,
required Object? extra,
}) {
bool debugGatherAllMatches = false;
assert(() {
debugGatherAllMatches = true;
return true;
}());
final List<List<GoRouteMatch>> result = <List<GoRouteMatch>>[];
// 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(<GoRouteMatch>[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<GoRouteMatch> 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(<GoRouteMatch>[match, ...subRouteMatch]);
}
// Should only reach here if there is a match.
if (debugGatherAllMatches) {
continue;
} else {
break;
}
}
if (result.isEmpty) {
throw _ParserError('no routes for location: $loc');
}
// 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<String, String> e in _nameToPath.entries) {
log.info(' ${e.key} => ${e.value}');
}
}
}
void _debugLogFullPathsFor(
List<GoRoute> 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);
}
}
) =>
// Use [SynchronousFuture] so that the initial url is processed
// synchronously and remove unwanted initial animations on deep-linking
SynchronousFuture<Uri>(Uri.parse(routeInformation.location!));
/// for use by the Router architecture as part of the RouteInformationParser
@override
RouteInformation restoreRouteInformation(List<GoRouteMatch> 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;
RouteInformation restoreRouteInformation(Uri configuration) =>
RouteInformation(location: configuration.toString());
}

View File

@ -1,120 +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/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
with WidgetsBindingObserver, ChangeNotifier {
/// Creates a [GoRouteInformationProvider].
GoRouteInformationProvider({
required RouteInformation initialRouteInformation,
Listenable? refreshListenable,
}) : _refreshListenable = refreshListenable,
_value = initialRouteInformation {
_refreshListenable?.addListener(notifyListeners);
}
final Listenable? _refreshListenable;
// ignore: unnecessary_non_null_assertion
static WidgetsBinding get _binding => WidgetsBinding.instance!;
@override
void routerReportsNewRouteInformation(RouteInformation routeInformation,
{RouteInformationReportingType type =
RouteInformationReportingType.none}) {
// Avoid adding a new history entry if the route is the same as before.
final bool replace = type == RouteInformationReportingType.neglect ||
(type == RouteInformationReportingType.none &&
_valueInEngine.location == routeInformation.location);
SystemNavigator.selectMultiEntryHistory();
// TODO(chunhtai): should report extra to to browser through state if
// possible.
SystemNavigator.routeInformationUpdated(
location: routeInformation.location!,
replace: replace,
);
_value = routeInformation;
_valueInEngine = routeInformation;
}
@override
RouteInformation get value => DebugGoRouteInformation(
location: _value.location,
state: _value.state,
);
RouteInformation _value;
set value(RouteInformation other) {
final bool shouldNotify =
value.location != other.location || value.state != other.state;
_value = other;
if (shouldNotify) {
notifyListeners();
}
}
RouteInformation _valueInEngine =
RouteInformation(location: _binding.platformDispatcher.defaultRouteName);
void _platformReportsNewRouteInformation(RouteInformation routeInformation) {
if (_value == routeInformation) {
return;
}
_value = routeInformation;
_valueInEngine = routeInformation;
notifyListeners();
}
@override
void addListener(VoidCallback listener) {
if (!hasListeners) {
_binding.addObserver(this);
}
super.addListener(listener);
}
@override
void removeListener(VoidCallback listener) {
super.removeListener(listener);
if (!hasListeners) {
_binding.removeObserver(this);
}
}
@override
void dispose() {
if (hasListeners) {
_binding.removeObserver(this);
}
_refreshListenable?.removeListener(notifyListeners);
super.dispose();
}
@override
Future<bool> didPushRouteInformation(
RouteInformation routeInformation) async {
assert(hasListeners);
print('_platformReportsNewRouteInformation $routeInformation');
_platformReportsNewRouteInformation(routeInformation);
return true;
}
@override
Future<bool> didPushRoute(String route) async {
assert(hasListeners);
_platformReportsNewRouteInformation(RouteInformation(location: route));
return true;
}
}
/// A debug class that is used for asserting the [GoRouteInformationProvider] is
/// in use with the [GoRouteInformationParser].
class DebugGoRouteInformation extends RouteInformation {
/// Creates
DebugGoRouteInformation({String? location, Object? state})
: super(location: location, state: state);
}

View File

@ -5,7 +5,7 @@
import 'package:flutter/foundation.dart';
import 'go_route.dart';
import 'go_route_information_parser.dart';
import 'go_router_delegate.dart';
import 'path_parser.dart';
/// Each GoRouteMatch instance represents an instance of a GoRoute for a
@ -22,8 +22,7 @@ class GoRouteMatch {
required this.extra,
required this.error,
this.pageKey,
}) : fullUriString = _addQueryParams(subloc, queryParams),
assert(subloc.startsWith('/')),
}) : assert(subloc.startsWith('/')),
assert(Uri.parse(subloc).queryParameters.isEmpty),
assert(fullpath.startsWith('/')),
assert(Uri.parse(fullpath).queryParameters.isEmpty),
@ -35,16 +34,61 @@ class GoRouteMatch {
return true;
}());
// ignore: public_member_api_docs
factory GoRouteMatch.matchNamed({
required GoRoute route,
required String name, // e.g. person
required String fullpath, // e.g. /family/:fid/person/:pid
required Map<String, String> params, // e.g. {'fid': 'f2', 'pid': 'p1'}
required Map<String, String> queryParams, // e.g. {'from': '/family/f2'}
required Object? extra,
}) {
assert(route.name != null);
assert(route.name!.toLowerCase() == name.toLowerCase());
assert(() {
// check that we have all the params we need
final List<String> paramNames = <String>[];
patternToRegExp(fullpath, paramNames);
for (final String paramName in paramNames) {
assert(params.containsKey(paramName),
'missing param "$paramName" for $fullpath');
}
// check that we have don't have extra params
for (final String key in params.keys) {
assert(paramNames.contains(key), 'unknown param "$key" for $fullpath');
}
return true;
}());
final Map<String, String> encodedParams = <String, String>{
for (final MapEntry<String, String> param in params.entries)
param.key: Uri.encodeComponent(param.value)
};
final String subloc = _locationFor(fullpath, encodedParams);
return GoRouteMatch(
route: route,
subloc: subloc,
fullpath: fullpath,
encodedParams: encodedParams,
queryParams: queryParams,
extra: extra,
error: null,
);
}
// ignore: public_member_api_docs
static GoRouteMatch? match({
required GoRoute route,
required String restLoc, // e.g. person/p1
required String parentSubloc, // e.g. /family/f2
required String path, // e.g. person/:pid
required String fullpath, // e.g. /family/:fid/person/:pid
required Map<String, String> queryParams,
required Object? extra,
}) {
assert(!route.path.contains('//'));
assert(!path.contains('//'));
final RegExpMatch? match = route.matchPatternAsPrefix(restLoc);
if (match == null) {
@ -52,9 +96,8 @@ class GoRouteMatch {
}
final Map<String, String> encodedParams = route.extractPathParams(match);
final String pathLoc = patternToPath(route.path, encodedParams);
final String subloc =
GoRouteInformationParser.concatenatePaths(parentSubloc, pathLoc);
final String pathLoc = _locationFor(path, encodedParams);
final String subloc = GoRouterDelegate.fullLocFor(parentSubloc, pathLoc);
return GoRouteMatch(
route: route,
subloc: subloc,
@ -90,18 +133,6 @@ class GoRouteMatch {
/// Optional value key of type string, to hold a unique reference to a page.
final ValueKey<String>? pageKey;
/// The full uri string
final String fullUriString; // e.g. /family/12?query=14
static String _addQueryParams(String loc, Map<String, String> queryParams) {
final Uri uri = Uri.parse(loc);
assert(uri.queryParameters.isEmpty);
return Uri(
path: uri.path,
queryParameters: queryParams.isEmpty ? null : queryParams)
.toString();
}
/// Parameters for the matched route, URI-decoded.
Map<String, String> get decodedParams => <String, String>{
for (final MapEntry<String, String> param in encodedParams.entries)
@ -111,4 +142,8 @@ class GoRouteMatch {
/// for use by the Router architecture as part of the GoRouteMatch
@override
String toString() => 'GoRouteMatch($fullpath, $encodedParams)';
/// expand a path w/ param slots using params, e.g. family/:fid => family/f1
static String _locationFor(String pattern, Map<String, String> params) =>
patternToPath(pattern, params);
}

View File

@ -6,8 +6,6 @@ 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';
@ -32,7 +30,7 @@ class GoRouter extends ChangeNotifier with NavigatorObserver {
Listenable? refreshListenable,
int redirectLimit = 5,
bool routerNeglect = false,
String? initialLocation,
String initialLocation = '/',
UrlPathStrategy? urlPathStrategy,
List<NavigatorObserver>? observers,
bool debugLogDiagnostics = false,
@ -44,31 +42,21 @@ class GoRouter extends ChangeNotifier with NavigatorObserver {
}
setLogging(enabled: debugLogDiagnostics);
WidgetsFlutterBinding.ensureInitialized();
final String _effectiveInitialLocation = initialLocation ??
// ignore: unnecessary_non_null_assertion
WidgetsBinding.instance!.platformDispatcher.defaultRouteName;
routeInformationParser = GoRouteInformationParser(
routes: routes,
topRedirect: redirect ?? (_) => null,
redirectLimit: redirectLimit,
debugRequireGoRouteInformationProvider: true,
);
routeInformationProvider = GoRouteInformationProvider(
initialRouteInformation:
RouteInformation(location: _effectiveInitialLocation),
refreshListenable: refreshListenable);
routerDelegate = GoRouterDelegate(
routeInformationParser,
routes: routes,
errorPageBuilder: errorPageBuilder,
errorBuilder: errorBuilder,
topRedirect: redirect ?? (_) => null,
redirectLimit: redirectLimit,
refreshListenable: refreshListenable,
routerNeglect: routerNeglect,
initUri: Uri.parse(initialLocation),
observers: <NavigatorObserver>[
...observers ?? <NavigatorObserver>[],
this
],
debugLogDiagnostics: debugLogDiagnostics,
restorationScopeId: restorationScopeId,
// wrap the returned Navigator to enable GoRouter.of(context).go() et al,
// allowing the caller to wrap the navigator themselves
@ -79,23 +67,17 @@ class GoRouter extends ChangeNotifier with NavigatorObserver {
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;
final GoRouteInformationParser routeInformationParser =
GoRouteInformationParser();
/// 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;
String get location => routerDelegate.currentConfiguration.toString();
/// Get a location from route name and parameters.
/// This is useful for redirecting to a named location.
@ -104,7 +86,7 @@ class GoRouter extends ChangeNotifier with NavigatorObserver {
Map<String, String> params = const <String, String>{},
Map<String, String> queryParams = const <String, String>{},
}) =>
routeInformationParser.namedLocation(
routerDelegate.namedLocation(
name,
params: params,
queryParams: queryParams,
@ -112,14 +94,8 @@ class GoRouter extends ChangeNotifier with NavigatorObserver {
/// 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);
}
void go(String location, {Object? extra}) =>
routerDelegate.go(location, extra: extra);
/// Navigate to a named route w/ optional parameters, e.g.
/// `name='person', params={'fid': 'f2', 'pid': 'p1'}`
@ -137,18 +113,8 @@ class GoRouter extends ChangeNotifier with NavigatorObserver {
/// 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<void>((List<GoRouteMatch> matches) {
routerDelegate.push(matches.last);
});
}
void push(String location, {Object? extra}) =>
routerDelegate.push(location, extra: extra);
/// Push a named route onto the page stack w/ optional parameters, e.g.
/// `name='person', params={'fid': 'f2', 'pid': 'p1'}`
@ -167,13 +133,7 @@ class GoRouter extends ChangeNotifier with NavigatorObserver {
void pop() => routerDelegate.pop();
/// Refresh the route.
void refresh() {
assert(() {
log.info('refreshing $location');
return true;
}());
routeInformationProvider.notifyListeners();
}
void refresh() => routerDelegate.refresh();
/// Set the app's URL path strategy (defaults to hash). call before runApp().
static void setUrlPathStrategy(UrlPathStrategy strategy) =>
@ -206,11 +166,4 @@ class GoRouter extends ChangeNotifier with NavigatorObserver {
@override
void didReplace({Route<dynamic>? newRoute, Route<dynamic>? oldRoute}) =>
notifyListeners();
@override
void dispose() {
routeInformationProvider.dispose();
routerDelegate.dispose();
super.dispose();
}
}

View File

@ -8,7 +8,7 @@ import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart';
import 'custom_transition_page.dart';
import 'go_route_information_parser.dart';
import 'go_route.dart';
import 'go_route_match.dart';
import 'go_router_cupertino.dart';
import 'go_router_error_page.dart';
@ -19,36 +19,76 @@ import 'route_data.dart';
import 'typedefs.dart';
/// GoRouter implementation of the RouterDelegate base class.
class GoRouterDelegate extends RouterDelegate<List<GoRouteMatch>>
with PopNavigatorRouterDelegateMixin<List<GoRouteMatch>>, ChangeNotifier {
class GoRouterDelegate extends RouterDelegate<Uri>
with
PopNavigatorRouterDelegateMixin<Uri>,
// ignore: prefer_mixin
ChangeNotifier {
/// Constructor for GoRouter's implementation of the
/// RouterDelegate base class.
GoRouterDelegate(
this._parser, {
GoRouterDelegate({
required this.builderWithNav,
required this.routes,
required this.errorPageBuilder,
required this.errorBuilder,
required this.topRedirect,
required this.redirectLimit,
required this.refreshListenable,
required Uri initUri,
required this.observers,
required this.debugLogDiagnostics,
required this.routerNeglect,
this.restorationScopeId,
});
}) : 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;
}()) {
// cache the set of named routes for fast lookup
_cacheNamedRoutes(routes, '', _namedMatches);
// TODO(chunhtai): remove this once namedLocation is removed from go_router.
final GoRouteInformationParser _parser;
// output known routes
_outputKnownRoutes();
// build the list of route matches
log.info('setting initial location $initUri');
_go(initUri.toString());
// when the listener changes, refresh the route
refreshListenable?.addListener(refresh);
}
/// Builder function for a go router with Navigator.
final GoRouterBuilderWithNav builderWithNav;
/// List of top level routes used by the go router delegate.
final List<GoRoute> routes;
/// Error page builder for the go router delegate.
final GoRouterPageBuilder? errorPageBuilder;
/// Error widget builder for the go router delegate.
final GoRouterWidgetBuilder? errorBuilder;
/// Top level page redirect.
final GoRouterRedirect topRedirect;
/// The limit for the number of consecutive redirects.
final int redirectLimit;
/// Listenable used to cause the router to refresh it's route.
final Listenable? refreshListenable;
/// NavigatorObserver used to receive change notifications when
/// navigation changes.
final List<NavigatorObserver> observers;
/// Set to true to log diagnostic info for your routes.
final bool debugLogDiagnostics;
/// Set to true to disable creating history entries on the web.
final bool routerNeglect;
@ -57,11 +97,78 @@ class GoRouterDelegate extends RouterDelegate<List<GoRouteMatch>>
final String? restorationScopeId;
final GlobalKey<NavigatorState> _key = GlobalKey<NavigatorState>();
List<GoRouteMatch> _matches = const <GoRouteMatch>[];
final List<GoRouteMatch> _matches = <GoRouteMatch>[];
final Map<String, GoRouteMatch> _namedMatches = <String, GoRouteMatch>{};
final Map<String, int> _pushCounts = <String, int>{};
void _cacheNamedRoutes(
List<GoRoute> routes,
String parentFullpath,
Map<String, GoRouteMatch> namedFullpaths,
) {
for (final GoRoute route in routes) {
final String fullpath = fullLocFor(parentFullpath, route.path);
if (route.name != null) {
final String name = route.name!.toLowerCase();
assert(!namedFullpaths.containsKey(name),
'duplication fullpaths for name "$name":${namedFullpaths[name]!.fullpath}, $fullpath');
// we only have a partial match until we have a location;
// we're really only caching the route and fullpath at this point
final GoRouteMatch match = GoRouteMatch(
route: route,
subloc: '/TBD',
fullpath: fullpath,
encodedParams: <String, String>{},
queryParams: <String, String>{},
extra: null,
error: null,
);
namedFullpaths[name] = match;
}
if (route.routes.isNotEmpty) {
_cacheNamedRoutes(route.routes, fullpath, namedFullpaths);
}
}
}
/// Get a location from route name and parameters.
/// This is useful for redirecting to a named location.
String namedLocation(
String name, {
required Map<String, String> params,
required Map<String, String> queryParams,
}) {
log.info('getting location for name: '
'"$name"'
'${params.isEmpty ? '' : ', params: $params'}'
'${queryParams.isEmpty ? '' : ', queryParams: $queryParams'}');
// find route and build up the full path along the way
final GoRouteMatch? match = _getNameRouteMatch(
name.toLowerCase(), // case-insensitive name matching
params: params,
queryParams: queryParams,
);
assert(match != null, 'unknown route name: $name');
assert(identical(match!.queryParams, queryParams));
return _addQueryParams(match!.subloc, queryParams);
}
/// Navigate to the given location.
void go(String location, {Object? extra}) {
log.info('going to $location');
_go(location, extra: extra);
notifyListeners();
}
/// Push the given location onto the page stack
void push(GoRouteMatch match) {
_matches.add(match);
void push(String location, {Object? extra}) {
log.info('pushing $location');
_push(location, extra: extra);
notifyListeners();
}
@ -73,17 +180,35 @@ class GoRouterDelegate extends RouterDelegate<List<GoRouteMatch>>
notifyListeners();
}
/// Refresh the current location, including re-evaluating redirections.
void refresh() {
log.info('refreshing $location');
_go(location, extra: _matches.last.extra);
notifyListeners();
}
/// Get the current location, e.g. /family/f2/person/p1
String get location =>
_addQueryParams(_matches.last.subloc, _matches.last.queryParams);
/// For internal use; visible for testing only.
@visibleForTesting
List<GoRouteMatch> get matches => _matches;
/// Dispose resources held by the router delegate.
@override
void dispose() {
refreshListenable?.removeListener(refresh);
super.dispose();
}
/// For use by the Router architecture as part of the RouterDelegate.
@override
GlobalKey<NavigatorState> get navigatorKey => _key;
/// For use by the Router architecture as part of the RouterDelegate.
@override
List<GoRouteMatch> get currentConfiguration => _matches;
Uri get currentConfiguration => Uri.parse(location);
/// For use by the Router architecture as part of the RouterDelegate.
@override
@ -91,17 +216,394 @@ class GoRouterDelegate extends RouterDelegate<List<GoRouteMatch>>
/// For use by the Router architecture as part of the RouterDelegate.
@override
Future<void> setNewRoutePath(List<GoRouteMatch> configuration) {
_matches = configuration;
Future<void> setInitialRoutePath(Uri configuration) {
// if the initial location is /, then use the dev initial location;
// otherwise, we're cruising to a deep link, so ignore dev initial location
final String config = configuration.toString();
if (config == '/') {
_go(location);
} else {
log.info('deep linking to $config');
_go(config);
}
// Use [SynchronousFuture] so that the initial url is processed
// synchronously and remove unwanted initial animations on deep-linking
return SynchronousFuture<void>(null);
}
/// For use by the Router architecture as part of the RouterDelegate.
@override
Future<void> setNewRoutePath(Uri configuration) async {
final String config = configuration.toString();
log.info('going to $config');
_go(config);
}
void _go(String location, {Object? extra}) {
final List<GoRouteMatch> matches =
_getLocRouteMatchesWithRedirects(location, extra: extra);
assert(matches.isNotEmpty);
// replace the stack of matches w/ the new ones
_matches
..clear()
..addAll(matches);
}
void _push(String location, {Object? extra}) {
final List<GoRouteMatch> matches =
_getLocRouteMatchesWithRedirects(location, extra: extra);
assert(matches.isNotEmpty);
final GoRouteMatch top = matches.last;
// remap the pageKey so allow any number of the same page on the stack
final String fullpath = top.fullpath;
final int count = (_pushCounts[fullpath] ?? 0) + 1;
_pushCounts[fullpath] = count;
final ValueKey<String> pageKey = ValueKey<String>('$fullpath-p$count');
final GoRouteMatch match = GoRouteMatch(
route: top.route,
subloc: top.subloc,
fullpath: top.fullpath,
encodedParams: top.encodedParams,
queryParams: top.queryParams,
extra: extra,
error: null,
pageKey: pageKey,
);
// add a new match onto the stack of matches
assert(matches.isNotEmpty);
_matches.add(match);
}
List<GoRouteMatch> _getLocRouteMatchesWithRedirects(
String location, {
required Object? extra,
}) {
// start redirecting from the initial location
List<GoRouteMatch> matches;
try {
// watch redirects for loops
final List<String> redirects = <String>[_canonicalUri(location)];
bool redirected(String? redir) {
if (redir == null) {
return false;
}
assert(Uri.tryParse(redir) != null, 'invalid redirect: $redir');
assert(
!redirects.contains(redir),
'redirect loop detected: ${<String>[
...redirects,
redir
].join(' => ')}');
assert(
redirects.length < redirectLimit,
'too many redirects: ${<String>[
...redirects,
redir
].join(' => ')}');
redirects.add(redir);
log.info('redirecting to $redir');
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,
),
),
)) {
continue;
}
// get stack of route matches
matches = _getLocRouteMatches(loc, extra: extra);
// merge new params to keep params from previously matched paths, e.g.
// /family/:fid/person/:pid provides fid and pid to person/:pid
Map<String, String> previouslyMatchedParams = <String, String>{};
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,
extra: extra,
),
),
)) {
continue;
}
// let Router know to update the address bar
// (the initial route is not a redirect)
if (redirects.length > 1) {
notifyListeners();
}
// 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
} catch (err, stack) {
log.severe('Exception during GoRouter navigation', err, stack);
// create a match that routes to the error page
final Exception error = err is Exception ? err : Exception(err);
final Uri uri = Uri.parse(location);
matches = <GoRouteMatch>[
GoRouteMatch(
subloc: uri.path,
fullpath: uri.path,
encodedParams: <String, String>{},
queryParams: uri.queryParameters,
extra: null,
error: error,
route: GoRoute(
path: location,
pageBuilder: (BuildContext context, GoRouterState state) =>
_errorPageBuilder(
context,
GoRouterState(
this,
location: state.location,
subloc: state.subloc,
name: state.name,
path: state.path,
error: error,
fullpath: state.path,
params: state.params,
queryParams: state.queryParams,
extra: state.extra,
),
),
),
),
];
}
assert(matches.isNotEmpty);
return matches;
}
List<GoRouteMatch> _getLocRouteMatches(
String location, {
Object? extra,
}) {
final Uri uri = Uri.parse(location);
final List<List<GoRouteMatch>> matchStacks = _getLocRouteMatchStacks(
loc: uri.path,
restLoc: uri.path,
routes: routes,
parentFullpath: '',
parentSubloc: '',
queryParams: uri.queryParameters,
extra: extra,
).toList();
assert(matchStacks.isNotEmpty, 'no routes for location: $location');
// 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 matchStacks.first;
}
/// turns a list of routes into a list of routes match stacks for the location
/// e.g. routes: <GoRoute>[
/// /
/// family/:fid
/// /login
/// ]
///
/// loc: /
/// stacks: [
/// matches: [
/// match(route.path=/, loc=/)
/// ]
/// ]
///
/// loc: /login
/// stacks: [
/// matches: [
/// match(route.path=/login, loc=login)
/// ]
/// ]
///
/// loc: /family/f2
/// stacks: [
/// matches: [
/// match(route.path=/, loc=/),
/// match(route.path=family/:fid, loc=family/f2, params=[fid=f2])
/// ]
/// ]
///
/// loc: /family/f2/person/p1
/// stacks: [
/// matches: [
/// match(route.path=/, loc=/),
/// match(route.path=family/:fid, loc=family/f2, params=[fid=f2])
/// match(route.path=person/:pid, loc=person/p1, params=[fid=f2, pid=p1])
/// ]
/// ]
///
/// A stack count of 0 means there's no match.
/// A stack count of >1 means there's a malformed set of routes.
///
/// NOTE: Uses recursion, which is why _getLocRouteMatchStacks calls this
/// function and does the actual error checking, using the returned stacks to
/// provide better errors
static Iterable<List<GoRouteMatch>> _getLocRouteMatchStacks({
required String loc,
required String restLoc,
required String parentSubloc,
required List<GoRoute> routes,
required String parentFullpath,
required Map<String, String> queryParams,
required Object? extra,
}) sync* {
// find the set of matches at this level of the tree
for (final GoRoute route in routes) {
final String fullpath = fullLocFor(parentFullpath, route.path);
final GoRouteMatch? match = GoRouteMatch.match(
route: route,
restLoc: restLoc,
parentSubloc: parentSubloc,
path: route.path,
fullpath: fullpath,
queryParams: queryParams,
extra: extra,
);
if (match == null) {
continue;
}
// if we have 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
if (match.subloc.toLowerCase() == loc.toLowerCase()) {
yield <GoRouteMatch>[match];
continue;
}
// if we have a partial match but no sub-routes, bail
if (route.routes.isEmpty) {
continue;
}
// otherwise recurse
final String childRestLoc =
loc.substring(match.subloc.length + (match.subloc == '/' ? 0 : 1));
assert(loc.startsWith(match.subloc));
assert(restLoc.isNotEmpty);
// if there's no sub-route matches, then we don't have a match for this
// location
final List<List<GoRouteMatch>> subRouteMatchStacks =
_getLocRouteMatchStacks(
loc: loc,
restLoc: childRestLoc,
parentSubloc: match.subloc,
routes: route.routes,
parentFullpath: fullpath,
queryParams: queryParams,
extra: extra,
).toList();
if (subRouteMatchStacks.isEmpty) {
continue;
}
// add the match to each of the sub-route match stacks and return them
for (final List<GoRouteMatch> stack in subRouteMatchStacks) {
yield <GoRouteMatch>[match, ...stack];
}
}
}
GoRouteMatch? _getNameRouteMatch(
String name, {
Map<String, String> params = const <String, String>{},
Map<String, String> queryParams = const <String, String>{},
Object? extra,
}) {
final GoRouteMatch? partialMatch = _namedMatches[name];
return partialMatch == null
? null
: GoRouteMatch.matchNamed(
name: name,
route: partialMatch.route,
fullpath: partialMatch.fullpath,
params: params,
queryParams: queryParams,
extra: extra,
);
}
// e.g.
// parentFullLoc: '', path => '/'
// parentFullLoc: '/', path => 'family/:fid' => '/family/:fid'
// parentFullLoc: '/', path => 'family/f2' => '/family/f2'
// parentFullLoc: '/family/f2', path => 'parent/p1' => '/family/f2/person/p1'
// ignore: public_member_api_docs
static String fullLocFor(String parentFullLoc, String path) {
// at the root, just return the path
if (parentFullLoc.isEmpty) {
assert(path.startsWith('/'));
assert(path == '/' || !path.endsWith('/'));
return path;
}
// not at the root, so append the parent path
assert(path.isNotEmpty);
assert(!path.startsWith('/'));
assert(!path.endsWith('/'));
return '${parentFullLoc == '/' ? '' : parentFullLoc}/$path';
}
Widget _builder(BuildContext context, Iterable<GoRouteMatch> matches) {
List<Page<dynamic>>? pages;
Exception? error;
final String location = matches.last.fullUriString;
try {
// build the stack of pages
if (routerNeglect) {
@ -118,10 +620,7 @@ class GoRouterDelegate extends RouterDelegate<List<GoRouteMatch>>
// `redirect` impl
// ignore: avoid_catches_without_on_clauses
} catch (err, stack) {
assert(() {
log.severe('Exception during GoRouter navigation', err, stack);
return true;
}());
log.severe('Exception during GoRouter navigation', err, stack);
// if there's an error, show an error page
error = err is Exception ? err : Exception(err);
@ -130,7 +629,7 @@ class GoRouterDelegate extends RouterDelegate<List<GoRouteMatch>>
_errorPageBuilder(
context,
GoRouterState(
_parser,
this,
location: location,
subloc: uri.path,
name: null,
@ -155,7 +654,7 @@ class GoRouterDelegate extends RouterDelegate<List<GoRouteMatch>>
return builderWithNav(
context,
GoRouterState(
_parser,
this,
location: location,
name: null, // no name available at the top level
// trim the query params off the subloc to match route.redirect
@ -216,24 +715,20 @@ class GoRouterDelegate extends RouterDelegate<List<GoRouteMatch>>
// get a page from the builder and associate it with a sub-location
final GoRouterState state = GoRouterState(
_parser,
location: match.fullUriString,
this,
location: location,
subloc: match.subloc,
name: match.route.name,
path: match.route.path,
fullpath: match.fullpath,
params: params,
error: match.error,
queryParams: match.queryParams,
extra: match.extra,
pageKey: match.pageKey, // push() remaps the page key for uniqueness
);
if (match.error != null) {
yield _errorPageBuilder(context, state);
break;
}
final GoRouterPageBuilder? pageBuilder = match.route.pageBuilder;
Page<dynamic>? page;
if (pageBuilder != null) {
page = pageBuilder(context, state);
@ -268,26 +763,17 @@ class GoRouterDelegate extends RouterDelegate<List<GoRouteMatch>>
final Element? elem = context is Element ? context : null;
if (elem != null && isMaterialApp(elem)) {
assert(() {
log.info('MaterialApp found');
return true;
}());
log.info('MaterialApp found');
_pageBuilderForAppType = pageBuilderForMaterialApp;
_errorBuilderForAppType = (BuildContext c, GoRouterState s) =>
GoRouterMaterialErrorScreen(s.error);
} else if (elem != null && isCupertinoApp(elem)) {
assert(() {
log.info('CupertinoApp found');
return true;
}());
log.info('CupertinoApp found');
_pageBuilderForAppType = pageBuilderForCupertinoApp;
_errorBuilderForAppType = (BuildContext c, GoRouterState s) =>
GoRouterCupertinoErrorScreen(s.error);
} else {
assert(() {
log.info('WidgetsApp found');
return true;
}());
log.info('WidgetsApp assumed');
_pageBuilderForAppType = pageBuilderForWidgetApp;
_errorBuilderForAppType =
(BuildContext c, GoRouterState s) => GoRouterErrorScreen(s.error);
@ -349,4 +835,54 @@ class GoRouterDelegate extends RouterDelegate<List<GoRouteMatch>>
errorBuilder ?? _errorBuilderForAppType!,
);
}
void _outputKnownRoutes() {
log.info('known full paths for routes:');
_outputFullPathsFor(routes, '', 0);
if (_namedMatches.isNotEmpty) {
log.info('known full paths for route names:');
for (final MapEntry<String, GoRouteMatch> e in _namedMatches.entries) {
log.info(' ${e.key} => ${e.value.fullpath}');
}
}
}
void _outputFullPathsFor(
List<GoRoute> routes,
String parentFullpath,
int depth,
) {
for (final GoRoute route in routes) {
final String fullpath = fullLocFor(parentFullpath, route.path);
log.info(' => ${''.padLeft(depth * 2)}$fullpath');
_outputFullPathsFor(route.routes, fullpath, depth + 1);
}
}
static 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;
}
static String _addQueryParams(String loc, Map<String, String> queryParams) {
final Uri uri = Uri.parse(loc);
assert(uri.queryParameters.isEmpty);
return _canonicalUri(
Uri(path: uri.path, queryParameters: queryParams).toString());
}
}

View File

@ -4,7 +4,7 @@
import 'package:flutter/foundation.dart';
import 'go_route_information_parser.dart';
import 'go_router_delegate.dart';
/// The route state during routing.
class GoRouterState {
@ -29,8 +29,7 @@ class GoRouterState {
: subloc),
assert((path ?? '').isEmpty == (fullpath ?? '').isEmpty);
// TODO(chunhtai): remove this once namedLocation is removed from go_router.
final GoRouteInformationParser _delegate;
final GoRouterDelegate _delegate;
/// The full location of the route, e.g. /family/f2/person/p1
final String location;
@ -68,8 +67,6 @@ class GoRouterState {
String name, {
Map<String, String> params = const <String, String>{},
Map<String, String> queryParams = const <String, String>{},
}) {
return _delegate.namedLocation(name,
params: params, queryParams: queryParams);
}
}) =>
_delegate.namedLocation(name, params: params, queryParams: queryParams);
}

View File

@ -1,12 +1,13 @@
name: go_router
description: A declarative router for Flutter based on Navigation 2 supporting
deep linking, data-driven routes and more
version: 4.0.0
version: 3.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
environment:
sdk: ">=2.12.0 <3.0.0"
flutter: ">=2.0.0"
dependencies:
collection: ^1.15.0

View File

@ -24,7 +24,6 @@ void main() {
);
await tester.pumpWidget(
MaterialApp.router(
routeInformationProvider: router.routeInformationProvider,
routeInformationParser: router.routeInformationParser,
routerDelegate: router.routerDelegate,
title: 'GoRouter Example',

View File

@ -51,7 +51,6 @@ WidgetTesterCallback testClickingTheButtonRedirectsToRoot({
Widget materialAppRouterBuilder(GoRouter router) {
return MaterialApp.router(
routeInformationProvider: router.routeInformationProvider,
routeInformationParser: router.routeInformationParser,
routerDelegate: router.routerDelegate,
title: 'GoRouter Example',
@ -60,7 +59,6 @@ Widget materialAppRouterBuilder(GoRouter router) {
Widget cupertinoAppRouterBuilder(GoRouter router) {
return CupertinoApp.router(
routeInformationProvider: router.routeInformationProvider,
routeInformationParser: router.routeInformationParser,
routerDelegate: router.routerDelegate,
title: 'GoRouter Example',

View File

@ -1,192 +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/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';
void main() {
test('GoRouteInformationParser can parse route', () async {
final List<GoRoute> routes = <GoRoute>[
GoRoute(
path: '/',
builder: (_, __) => const Placeholder(),
routes: <GoRoute>[
GoRoute(
path: 'abc',
builder: (_, __) => const Placeholder(),
),
],
),
];
final GoRouteInformationParser parser = GoRouteInformationParser(
routes: routes,
redirectLimit: 100,
topRedirect: (_) => null,
);
List<GoRouteMatch> matches = await parser
.parseRouteInformation(const RouteInformation(location: '/'));
expect(matches.length, 1);
expect(matches[0].queryParams.isEmpty, isTrue);
expect(matches[0].extra, isNull);
expect(matches[0].fullUriString, '/');
expect(matches[0].subloc, '/');
expect(matches[0].route, routes[0]);
final Object extra = Object();
matches = await parser.parseRouteInformation(
RouteInformation(location: '/abc?def=ghi', state: extra));
expect(matches.length, 2);
expect(matches[0].queryParams.length, 1);
expect(matches[0].queryParams['def'], 'ghi');
expect(matches[0].extra, extra);
expect(matches[0].fullUriString, '/?def=ghi');
expect(matches[0].subloc, '/');
expect(matches[0].route, routes[0]);
expect(matches[1].queryParams.length, 1);
expect(matches[1].queryParams['def'], 'ghi');
expect(matches[1].extra, extra);
expect(matches[1].fullUriString, '/abc?def=ghi');
expect(matches[1].subloc, '/abc');
expect(matches[1].route, routes[0].routes[0]);
});
test('GoRouteInformationParser returns error when unknown route', () async {
final List<GoRoute> routes = <GoRoute>[
GoRoute(
path: '/',
builder: (_, __) => const Placeholder(),
routes: <GoRoute>[
GoRoute(
path: 'abc',
builder: (_, __) => const Placeholder(),
),
],
),
];
final GoRouteInformationParser parser = GoRouteInformationParser(
routes: routes,
redirectLimit: 100,
topRedirect: (_) => null,
);
final List<GoRouteMatch> matches = await parser
.parseRouteInformation(const RouteInformation(location: '/def'));
expect(matches.length, 1);
expect(matches[0].queryParams.isEmpty, isTrue);
expect(matches[0].extra, isNull);
expect(matches[0].fullUriString, '/def');
expect(matches[0].subloc, '/def');
expect(matches[0].error!.toString(),
'Exception: no routes for location: /def');
});
test('GoRouteInformationParser can work with route parameters', () async {
final List<GoRoute> routes = <GoRoute>[
GoRoute(
path: '/',
builder: (_, __) => const Placeholder(),
routes: <GoRoute>[
GoRoute(
path: ':uid/family/:fid',
builder: (_, __) => const Placeholder(),
),
],
),
];
final GoRouteInformationParser parser = GoRouteInformationParser(
routes: routes,
redirectLimit: 100,
topRedirect: (_) => null,
);
final List<GoRouteMatch> matches = await parser.parseRouteInformation(
const RouteInformation(location: '/123/family/456'));
expect(matches.length, 2);
expect(matches[0].queryParams.isEmpty, isTrue);
expect(matches[0].extra, isNull);
expect(matches[0].fullUriString, '/');
expect(matches[0].subloc, '/');
expect(matches[1].queryParams.isEmpty, isTrue);
expect(matches[1].extra, isNull);
expect(matches[1].fullUriString, '/123/family/456');
expect(matches[1].subloc, '/123/family/456');
expect(matches[1].encodedParams.length, 2);
expect(matches[1].encodedParams['uid'], '123');
expect(matches[1].encodedParams['fid'], '456');
});
test('GoRouteInformationParser can do top level redirect', () async {
final List<GoRoute> routes = <GoRoute>[
GoRoute(
path: '/',
builder: (_, __) => const Placeholder(),
routes: <GoRoute>[
GoRoute(
path: ':uid/family/:fid',
builder: (_, __) => const Placeholder(),
),
],
),
];
final GoRouteInformationParser parser = GoRouteInformationParser(
routes: routes,
redirectLimit: 100,
topRedirect: (GoRouterState state) {
if (state.location != '/123/family/345') {
return '/123/family/345';
}
return null;
},
);
final List<GoRouteMatch> matches = await parser
.parseRouteInformation(const RouteInformation(location: '/random/uri'));
expect(matches.length, 2);
expect(matches[0].fullUriString, '/');
expect(matches[0].subloc, '/');
expect(matches[1].fullUriString, '/123/family/345');
expect(matches[1].subloc, '/123/family/345');
});
test('GoRouteInformationParser can do route level redirect', () async {
final List<GoRoute> routes = <GoRoute>[
GoRoute(
path: '/',
builder: (_, __) => const Placeholder(),
routes: <GoRoute>[
GoRoute(
path: ':uid/family/:fid',
builder: (_, __) => const Placeholder(),
),
GoRoute(
path: 'redirect',
redirect: (_) => '/123/family/345',
builder: (_, __) => throw UnimplementedError(),
),
],
),
];
final GoRouteInformationParser parser = GoRouteInformationParser(
routes: routes,
redirectLimit: 100,
topRedirect: (_) => null,
);
final List<GoRouteMatch> matches = await parser
.parseRouteInformation(const RouteInformation(location: '/redirect'));
expect(matches.length, 2);
expect(matches[0].fullUriString, '/');
expect(matches[0].subloc, '/');
expect(matches[1].fullUriString, '/123/family/345');
expect(matches[1].subloc, '/123/family/345');
});
}

View File

@ -6,12 +6,12 @@ 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_delegate.dart';
import 'package:go_router/src/go_router_error_page.dart';
Future<GoRouter> createGoRouter(
WidgetTester tester, {
GoRouterDelegate createGoRouterDelegate({
Listenable? refreshListenable,
}) async {
}) {
final GoRouter router = GoRouter(
initialLocation: '/',
routes: <GoRoute>[
@ -23,32 +23,26 @@ Future<GoRouter> createGoRouter(
],
refreshListenable: refreshListenable,
);
await tester.pumpWidget(MaterialApp.router(
routeInformationProvider: router.routeInformationProvider,
routeInformationParser: router.routeInformationParser,
routerDelegate: router.routerDelegate));
return router;
return router.routerDelegate;
}
void main() {
group('pop', () {
testWidgets('removes the last element', (WidgetTester tester) async {
final GoRouter goRouter = await createGoRouter(tester)
..push('/error');
goRouter.routerDelegate.addListener(expectAsync0(() {}));
final GoRouteMatch last = goRouter.routerDelegate.matches.last;
goRouter.routerDelegate.pop();
expect(goRouter.routerDelegate.matches.length, 1);
expect(goRouter.routerDelegate.matches.contains(last), false);
test('removes the last element', () {
final GoRouterDelegate delegate = createGoRouterDelegate()
..push('/error')
..addListener(expectAsync0(() {}));
final GoRouteMatch last = delegate.matches.last;
delegate.pop();
expect(delegate.matches.length, 1);
expect(delegate.matches.contains(last), false);
});
testWidgets('throws when it pops more than matches count',
(WidgetTester tester) async {
final GoRouter goRouter = await createGoRouter(tester)
test('throws when it pops more than matches count', () {
final GoRouterDelegate delegate = createGoRouterDelegate()
..push('/error');
expect(
() => goRouter.routerDelegate
() => delegate
..pop()
..pop(),
throwsA(isAssertionError),
@ -56,13 +50,9 @@ void main() {
});
});
testWidgets('dispose unsubscribes from refreshListenable',
(WidgetTester tester) async {
test('dispose unsubscribes from refreshListenable', () {
final FakeRefreshListenable refreshListenable = FakeRefreshListenable();
final GoRouter goRouter =
await createGoRouter(tester, refreshListenable: refreshListenable);
await tester.pumpWidget(Container());
goRouter.dispose();
createGoRouterDelegate(refreshListenable: refreshListenable).dispose();
expect(refreshListenable.unsubscribed, true);
});
}

File diff suppressed because it is too large Load Diff