mirror of
https://github.com/flutter/packages.git
synced 2025-06-29 14:18:54 +08:00
This reverts commit 2c02052ce8ba83edee76fef41a2475edab824c17.
This commit is contained in:
@ -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
|
||||
|
||||
|
@ -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',
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
),
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -27,7 +27,6 @@ class App extends StatelessWidget {
|
||||
home: NoExtraParamOnWebScreen(),
|
||||
)
|
||||
: MaterialApp.router(
|
||||
routeInformationProvider: _router.routeInformationProvider,
|
||||
routeInformationParser: _router.routeInformationParser,
|
||||
routerDelegate: _router.routerDelegate,
|
||||
title: title,
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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());
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
@ -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);
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -24,7 +24,6 @@ void main() {
|
||||
);
|
||||
await tester.pumpWidget(
|
||||
MaterialApp.router(
|
||||
routeInformationProvider: router.routeInformationProvider,
|
||||
routeInformationParser: router.routeInformationParser,
|
||||
routerDelegate: router.routerDelegate,
|
||||
title: 'GoRouter Example',
|
||||
|
@ -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',
|
||||
|
@ -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');
|
||||
});
|
||||
}
|
@ -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
Reference in New Issue
Block a user