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 ## 3.1.1
- Uses first match if there are more than one route to match. [ [#99833](https://github.com/flutter/flutter/issues/99833) - Uses first match if there are more than one route to match. [ [#99833](https://github.com/flutter/flutter/issues/99833)
## 3.1.0 ## 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 ## 3.0.7

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -4,435 +4,20 @@
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.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 /// GoRouter implementation of the RouteInformationParser base class
class GoRouteInformationParser class GoRouteInformationParser extends RouteInformationParser<Uri> {
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';
}
/// for use by the Router architecture as part of the RouteInformationParser /// for use by the Router architecture as part of the RouteInformationParser
@override @override
Future<List<GoRouteMatch>> parseRouteInformation( Future<Uri> parseRouteInformation(
RouteInformation routeInformation, RouteInformation routeInformation,
) { ) =>
assert(() { // Use [SynchronousFuture] so that the initial url is processed
if (debugRequireGoRouteInformationProvider) { // synchronously and remove unwanted initial animations on deep-linking
assert( SynchronousFuture<Uri>(Uri.parse(routeInformation.location!));
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);
}
}
/// for use by the Router architecture as part of the RouteInformationParser /// for use by the Router architecture as part of the RouteInformationParser
@override @override
RouteInformation restoreRouteInformation(List<GoRouteMatch> configuration) { RouteInformation restoreRouteInformation(Uri configuration) =>
return RouteInformation( RouteInformation(location: configuration.toString());
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;
} }

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 'package:flutter/foundation.dart';
import 'go_route.dart'; import 'go_route.dart';
import 'go_route_information_parser.dart'; import 'go_router_delegate.dart';
import 'path_parser.dart'; import 'path_parser.dart';
/// Each GoRouteMatch instance represents an instance of a GoRoute for a /// Each GoRouteMatch instance represents an instance of a GoRoute for a
@ -22,8 +22,7 @@ class GoRouteMatch {
required this.extra, required this.extra,
required this.error, required this.error,
this.pageKey, this.pageKey,
}) : fullUriString = _addQueryParams(subloc, queryParams), }) : assert(subloc.startsWith('/')),
assert(subloc.startsWith('/')),
assert(Uri.parse(subloc).queryParameters.isEmpty), assert(Uri.parse(subloc).queryParameters.isEmpty),
assert(fullpath.startsWith('/')), assert(fullpath.startsWith('/')),
assert(Uri.parse(fullpath).queryParameters.isEmpty), assert(Uri.parse(fullpath).queryParameters.isEmpty),
@ -35,16 +34,61 @@ class GoRouteMatch {
return true; 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 // ignore: public_member_api_docs
static GoRouteMatch? match({ static GoRouteMatch? match({
required GoRoute route, required GoRoute route,
required String restLoc, // e.g. person/p1 required String restLoc, // e.g. person/p1
required String parentSubloc, // e.g. /family/f2 required String parentSubloc, // e.g. /family/f2
required String path, // e.g. person/:pid
required String fullpath, // e.g. /family/:fid/person/:pid required String fullpath, // e.g. /family/:fid/person/:pid
required Map<String, String> queryParams, required Map<String, String> queryParams,
required Object? extra, required Object? extra,
}) { }) {
assert(!route.path.contains('//')); assert(!path.contains('//'));
final RegExpMatch? match = route.matchPatternAsPrefix(restLoc); final RegExpMatch? match = route.matchPatternAsPrefix(restLoc);
if (match == null) { if (match == null) {
@ -52,9 +96,8 @@ class GoRouteMatch {
} }
final Map<String, String> encodedParams = route.extractPathParams(match); final Map<String, String> encodedParams = route.extractPathParams(match);
final String pathLoc = patternToPath(route.path, encodedParams); final String pathLoc = _locationFor(path, encodedParams);
final String subloc = final String subloc = GoRouterDelegate.fullLocFor(parentSubloc, pathLoc);
GoRouteInformationParser.concatenatePaths(parentSubloc, pathLoc);
return GoRouteMatch( return GoRouteMatch(
route: route, route: route,
subloc: subloc, subloc: subloc,
@ -90,18 +133,6 @@ class GoRouteMatch {
/// Optional value key of type string, to hold a unique reference to a page. /// Optional value key of type string, to hold a unique reference to a page.
final ValueKey<String>? pageKey; 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. /// Parameters for the matched route, URI-decoded.
Map<String, String> get decodedParams => <String, String>{ Map<String, String> get decodedParams => <String, String>{
for (final MapEntry<String, String> param in encodedParams.entries) 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 /// for use by the Router architecture as part of the GoRouteMatch
@override @override
String toString() => 'GoRouteMatch($fullpath, $encodedParams)'; 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.dart';
import 'go_route_information_parser.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_delegate.dart';
import 'go_router_state.dart'; import 'go_router_state.dart';
import 'inherited_go_router.dart'; import 'inherited_go_router.dart';
@ -32,7 +30,7 @@ class GoRouter extends ChangeNotifier with NavigatorObserver {
Listenable? refreshListenable, Listenable? refreshListenable,
int redirectLimit = 5, int redirectLimit = 5,
bool routerNeglect = false, bool routerNeglect = false,
String? initialLocation, String initialLocation = '/',
UrlPathStrategy? urlPathStrategy, UrlPathStrategy? urlPathStrategy,
List<NavigatorObserver>? observers, List<NavigatorObserver>? observers,
bool debugLogDiagnostics = false, bool debugLogDiagnostics = false,
@ -44,31 +42,21 @@ class GoRouter extends ChangeNotifier with NavigatorObserver {
} }
setLogging(enabled: debugLogDiagnostics); 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( routerDelegate = GoRouterDelegate(
routeInformationParser, routes: routes,
errorPageBuilder: errorPageBuilder, errorPageBuilder: errorPageBuilder,
errorBuilder: errorBuilder, errorBuilder: errorBuilder,
topRedirect: redirect ?? (_) => null,
redirectLimit: redirectLimit,
refreshListenable: refreshListenable,
routerNeglect: routerNeglect, routerNeglect: routerNeglect,
initUri: Uri.parse(initialLocation),
observers: <NavigatorObserver>[ observers: <NavigatorObserver>[
...observers ?? <NavigatorObserver>[], ...observers ?? <NavigatorObserver>[],
this this
], ],
debugLogDiagnostics: debugLogDiagnostics,
restorationScopeId: restorationScopeId, restorationScopeId: restorationScopeId,
// wrap the returned Navigator to enable GoRouter.of(context).go() et al, // wrap the returned Navigator to enable GoRouter.of(context).go() et al,
// allowing the caller to wrap the navigator themselves // 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, 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. /// 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. /// The router delegate used by the go router.
late final GoRouterDelegate routerDelegate; late final GoRouterDelegate routerDelegate;
/// The route information provider used by the go router.
late final GoRouteInformationProvider routeInformationProvider;
/// Get the current location. /// 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. /// Get a location from route name and parameters.
/// This is useful for redirecting to a named location. /// 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> params = const <String, String>{},
Map<String, String> queryParams = const <String, String>{}, Map<String, String> queryParams = const <String, String>{},
}) => }) =>
routeInformationParser.namedLocation( routerDelegate.namedLocation(
name, name,
params: params, params: params,
queryParams: queryParams, queryParams: queryParams,
@ -112,14 +94,8 @@ class GoRouter extends ChangeNotifier with NavigatorObserver {
/// Navigate to a URI location w/ optional query parameters, e.g. /// Navigate to a URI location w/ optional query parameters, e.g.
/// `/family/f2/person/p1?color=blue` /// `/family/f2/person/p1?color=blue`
void go(String location, {Object? extra}) { void go(String location, {Object? extra}) =>
assert(() { routerDelegate.go(location, extra: extra);
log.info('going to $location');
return true;
}());
routeInformationProvider.value =
RouteInformation(location: location, state: extra);
}
/// Navigate to a named route w/ optional parameters, e.g. /// Navigate to a named route w/ optional parameters, e.g.
/// `name='person', params={'fid': 'f2', 'pid': 'p1'}` /// `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. /// Push a URI location onto the page stack w/ optional query parameters, e.g.
/// `/family/f2/person/p1?color=blue` /// `/family/f2/person/p1?color=blue`
void push(String location, {Object? extra}) { void push(String location, {Object? extra}) =>
assert(() { routerDelegate.push(location, extra: extra);
log.info('pushing $location');
return true;
}());
routeInformationParser
.parseRouteInformation(
DebugGoRouteInformation(location: location, state: extra))
.then<void>((List<GoRouteMatch> matches) {
routerDelegate.push(matches.last);
});
}
/// Push a named route onto the page stack w/ optional parameters, e.g. /// Push a named route onto the page stack w/ optional parameters, e.g.
/// `name='person', params={'fid': 'f2', 'pid': 'p1'}` /// `name='person', params={'fid': 'f2', 'pid': 'p1'}`
@ -167,13 +133,7 @@ class GoRouter extends ChangeNotifier with NavigatorObserver {
void pop() => routerDelegate.pop(); void pop() => routerDelegate.pop();
/// Refresh the route. /// Refresh the route.
void refresh() { void refresh() => routerDelegate.refresh();
assert(() {
log.info('refreshing $location');
return true;
}());
routeInformationProvider.notifyListeners();
}
/// Set the app's URL path strategy (defaults to hash). call before runApp(). /// Set the app's URL path strategy (defaults to hash). call before runApp().
static void setUrlPathStrategy(UrlPathStrategy strategy) => static void setUrlPathStrategy(UrlPathStrategy strategy) =>
@ -206,11 +166,4 @@ class GoRouter extends ChangeNotifier with NavigatorObserver {
@override @override
void didReplace({Route<dynamic>? newRoute, Route<dynamic>? oldRoute}) => void didReplace({Route<dynamic>? newRoute, Route<dynamic>? oldRoute}) =>
notifyListeners(); 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 'package:flutter/widgets.dart';
import 'custom_transition_page.dart'; import 'custom_transition_page.dart';
import 'go_route_information_parser.dart'; import 'go_route.dart';
import 'go_route_match.dart'; import 'go_route_match.dart';
import 'go_router_cupertino.dart'; import 'go_router_cupertino.dart';
import 'go_router_error_page.dart'; import 'go_router_error_page.dart';
@ -19,36 +19,76 @@ import 'route_data.dart';
import 'typedefs.dart'; import 'typedefs.dart';
/// GoRouter implementation of the RouterDelegate base class. /// GoRouter implementation of the RouterDelegate base class.
class GoRouterDelegate extends RouterDelegate<List<GoRouteMatch>> class GoRouterDelegate extends RouterDelegate<Uri>
with PopNavigatorRouterDelegateMixin<List<GoRouteMatch>>, ChangeNotifier { with
PopNavigatorRouterDelegateMixin<Uri>,
// ignore: prefer_mixin
ChangeNotifier {
/// Constructor for GoRouter's implementation of the /// Constructor for GoRouter's implementation of the
/// RouterDelegate base class. /// RouterDelegate base class.
GoRouterDelegate( GoRouterDelegate({
this._parser, {
required this.builderWithNav, required this.builderWithNav,
required this.routes,
required this.errorPageBuilder, required this.errorPageBuilder,
required this.errorBuilder, required this.errorBuilder,
required this.topRedirect,
required this.redirectLimit,
required this.refreshListenable,
required Uri initUri,
required this.observers, required this.observers,
required this.debugLogDiagnostics,
required this.routerNeglect, required this.routerNeglect,
this.restorationScopeId, 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. // output known routes
final GoRouteInformationParser _parser; _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. /// Builder function for a go router with Navigator.
final GoRouterBuilderWithNav builderWithNav; 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. /// Error page builder for the go router delegate.
final GoRouterPageBuilder? errorPageBuilder; final GoRouterPageBuilder? errorPageBuilder;
/// Error widget builder for the go router delegate. /// Error widget builder for the go router delegate.
final GoRouterWidgetBuilder? errorBuilder; 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 /// NavigatorObserver used to receive change notifications when
/// navigation changes. /// navigation changes.
final List<NavigatorObserver> observers; 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. /// Set to true to disable creating history entries on the web.
final bool routerNeglect; final bool routerNeglect;
@ -57,11 +97,78 @@ class GoRouterDelegate extends RouterDelegate<List<GoRouteMatch>>
final String? restorationScopeId; final String? restorationScopeId;
final GlobalKey<NavigatorState> _key = GlobalKey<NavigatorState>(); 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 /// Push the given location onto the page stack
void push(GoRouteMatch match) { void push(String location, {Object? extra}) {
_matches.add(match); log.info('pushing $location');
_push(location, extra: extra);
notifyListeners(); notifyListeners();
} }
@ -73,17 +180,35 @@ class GoRouterDelegate extends RouterDelegate<List<GoRouteMatch>>
notifyListeners(); 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. /// For internal use; visible for testing only.
@visibleForTesting @visibleForTesting
List<GoRouteMatch> get matches => _matches; 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. /// For use by the Router architecture as part of the RouterDelegate.
@override @override
GlobalKey<NavigatorState> get navigatorKey => _key; GlobalKey<NavigatorState> get navigatorKey => _key;
/// For use by the Router architecture as part of the RouterDelegate. /// For use by the Router architecture as part of the RouterDelegate.
@override @override
List<GoRouteMatch> get currentConfiguration => _matches; Uri get currentConfiguration => Uri.parse(location);
/// For use by the Router architecture as part of the RouterDelegate. /// For use by the Router architecture as part of the RouterDelegate.
@override @override
@ -91,17 +216,394 @@ class GoRouterDelegate extends RouterDelegate<List<GoRouteMatch>>
/// For use by the Router architecture as part of the RouterDelegate. /// For use by the Router architecture as part of the RouterDelegate.
@override @override
Future<void> setNewRoutePath(List<GoRouteMatch> configuration) { Future<void> setInitialRoutePath(Uri configuration) {
_matches = 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 // Use [SynchronousFuture] so that the initial url is processed
// synchronously and remove unwanted initial animations on deep-linking // synchronously and remove unwanted initial animations on deep-linking
return SynchronousFuture<void>(null); 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) { Widget _builder(BuildContext context, Iterable<GoRouteMatch> matches) {
List<Page<dynamic>>? pages; List<Page<dynamic>>? pages;
Exception? error; Exception? error;
final String location = matches.last.fullUriString;
try { try {
// build the stack of pages // build the stack of pages
if (routerNeglect) { if (routerNeglect) {
@ -118,10 +620,7 @@ class GoRouterDelegate extends RouterDelegate<List<GoRouteMatch>>
// `redirect` impl // `redirect` impl
// ignore: avoid_catches_without_on_clauses // ignore: avoid_catches_without_on_clauses
} catch (err, stack) { } catch (err, stack) {
assert(() { log.severe('Exception during GoRouter navigation', err, stack);
log.severe('Exception during GoRouter navigation', err, stack);
return true;
}());
// if there's an error, show an error page // if there's an error, show an error page
error = err is Exception ? err : Exception(err); error = err is Exception ? err : Exception(err);
@ -130,7 +629,7 @@ class GoRouterDelegate extends RouterDelegate<List<GoRouteMatch>>
_errorPageBuilder( _errorPageBuilder(
context, context,
GoRouterState( GoRouterState(
_parser, this,
location: location, location: location,
subloc: uri.path, subloc: uri.path,
name: null, name: null,
@ -155,7 +654,7 @@ class GoRouterDelegate extends RouterDelegate<List<GoRouteMatch>>
return builderWithNav( return builderWithNav(
context, context,
GoRouterState( GoRouterState(
_parser, this,
location: location, location: location,
name: null, // no name available at the top level name: null, // no name available at the top level
// trim the query params off the subloc to match route.redirect // 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 // get a page from the builder and associate it with a sub-location
final GoRouterState state = GoRouterState( final GoRouterState state = GoRouterState(
_parser, this,
location: match.fullUriString, location: location,
subloc: match.subloc, subloc: match.subloc,
name: match.route.name, name: match.route.name,
path: match.route.path, path: match.route.path,
fullpath: match.fullpath, fullpath: match.fullpath,
params: params, params: params,
error: match.error,
queryParams: match.queryParams, queryParams: match.queryParams,
extra: match.extra, extra: match.extra,
pageKey: match.pageKey, // push() remaps the page key for uniqueness 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; final GoRouterPageBuilder? pageBuilder = match.route.pageBuilder;
Page<dynamic>? page; Page<dynamic>? page;
if (pageBuilder != null) { if (pageBuilder != null) {
page = pageBuilder(context, state); page = pageBuilder(context, state);
@ -268,26 +763,17 @@ class GoRouterDelegate extends RouterDelegate<List<GoRouteMatch>>
final Element? elem = context is Element ? context : null; final Element? elem = context is Element ? context : null;
if (elem != null && isMaterialApp(elem)) { if (elem != null && isMaterialApp(elem)) {
assert(() { log.info('MaterialApp found');
log.info('MaterialApp found');
return true;
}());
_pageBuilderForAppType = pageBuilderForMaterialApp; _pageBuilderForAppType = pageBuilderForMaterialApp;
_errorBuilderForAppType = (BuildContext c, GoRouterState s) => _errorBuilderForAppType = (BuildContext c, GoRouterState s) =>
GoRouterMaterialErrorScreen(s.error); GoRouterMaterialErrorScreen(s.error);
} else if (elem != null && isCupertinoApp(elem)) { } else if (elem != null && isCupertinoApp(elem)) {
assert(() { log.info('CupertinoApp found');
log.info('CupertinoApp found');
return true;
}());
_pageBuilderForAppType = pageBuilderForCupertinoApp; _pageBuilderForAppType = pageBuilderForCupertinoApp;
_errorBuilderForAppType = (BuildContext c, GoRouterState s) => _errorBuilderForAppType = (BuildContext c, GoRouterState s) =>
GoRouterCupertinoErrorScreen(s.error); GoRouterCupertinoErrorScreen(s.error);
} else { } else {
assert(() { log.info('WidgetsApp assumed');
log.info('WidgetsApp found');
return true;
}());
_pageBuilderForAppType = pageBuilderForWidgetApp; _pageBuilderForAppType = pageBuilderForWidgetApp;
_errorBuilderForAppType = _errorBuilderForAppType =
(BuildContext c, GoRouterState s) => GoRouterErrorScreen(s.error); (BuildContext c, GoRouterState s) => GoRouterErrorScreen(s.error);
@ -349,4 +835,54 @@ class GoRouterDelegate extends RouterDelegate<List<GoRouteMatch>>
errorBuilder ?? _errorBuilderForAppType!, 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 'package:flutter/foundation.dart';
import 'go_route_information_parser.dart'; import 'go_router_delegate.dart';
/// The route state during routing. /// The route state during routing.
class GoRouterState { class GoRouterState {
@ -29,8 +29,7 @@ class GoRouterState {
: subloc), : subloc),
assert((path ?? '').isEmpty == (fullpath ?? '').isEmpty); assert((path ?? '').isEmpty == (fullpath ?? '').isEmpty);
// TODO(chunhtai): remove this once namedLocation is removed from go_router. final GoRouterDelegate _delegate;
final GoRouteInformationParser _delegate;
/// The full location of the route, e.g. /family/f2/person/p1 /// The full location of the route, e.g. /family/f2/person/p1
final String location; final String location;
@ -68,8 +67,6 @@ class GoRouterState {
String name, { String name, {
Map<String, String> params = const <String, String>{}, Map<String, String> params = const <String, String>{},
Map<String, String> queryParams = const <String, String>{}, Map<String, String> queryParams = const <String, String>{},
}) { }) =>
return _delegate.namedLocation(name, _delegate.namedLocation(name, params: params, queryParams: queryParams);
params: params, queryParams: queryParams);
}
} }

View File

@ -1,12 +1,13 @@
name: go_router name: go_router
description: A declarative router for Flutter based on Navigation 2 supporting description: A declarative router for Flutter based on Navigation 2 supporting
deep linking, data-driven routes and more 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 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 issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+go_router%22
environment: environment:
sdk: ">=2.12.0 <3.0.0" sdk: ">=2.12.0 <3.0.0"
flutter: ">=2.0.0"
dependencies: dependencies:
collection: ^1.15.0 collection: ^1.15.0

View File

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

View File

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

File diff suppressed because it is too large Load Diff