[go_router] fixes pop and push to update urls correctly (#2904)

* [go_router] fixes pop and push to update urls correctly

* bump version
This commit is contained in:
chunhtai
2022-12-07 13:22:34 -08:00
committed by GitHub
parent 02de151969
commit 250adea8b2
8 changed files with 194 additions and 6 deletions

View File

@ -1,3 +1,7 @@
## 5.2.2
- Fixes `pop` and `push` to update urls correctly.
## 5.2.1
- Refactors `GoRouter.pop` to be able to pop individual pageless route with result.

View File

@ -133,6 +133,7 @@ class GoRouterDelegate extends RouterDelegate<RouteMatchList>
return false;
}
_matchList.pop();
notifyListeners();
assert(() {
_debugAssertMatchListNotEmpty();
return true;

View File

@ -48,7 +48,7 @@ class RouteMatcher {
/// The list of [RouteMatch] objects.
class RouteMatchList {
/// RouteMatchList constructor.
RouteMatchList(List<RouteMatch> matches, this.uri, this.pathParameters)
RouteMatchList(List<RouteMatch> matches, this._uri, this.pathParameters)
: _matches = matches,
fullpath = _generateFullPath(matches);
@ -82,7 +82,8 @@ class RouteMatchList {
final Map<String, String> pathParameters;
/// The uri of the current match.
final Uri uri;
Uri get uri => _uri;
Uri _uri;
/// Returns true if there are no matches.
bool get isEmpty => _matches.isEmpty;
@ -97,8 +98,11 @@ class RouteMatchList {
/// Removes the last match.
void pop() {
if (_matches.last.route is GoRoute) {
final GoRoute route = _matches.last.route as GoRoute;
_uri = _uri.replace(path: removePatternFromPath(route.path, _uri.path));
}
_matches.removeLast();
// Also pop ShellRoutes when there are no subsequent route matches
while (_matches.isNotEmpty && _matches.last.route is ShellRoute) {
_matches.removeLast();

View File

@ -8,6 +8,7 @@ import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart';
import 'configuration.dart';
import 'delegate.dart';
import 'information_provider.dart';
import 'logging.dart';
import 'matching.dart';
@ -98,6 +99,10 @@ class GoRouteInformationParser extends RouteInformationParser<RouteMatchList> {
/// for use by the Router architecture as part of the RouteInformationParser
@override
RouteInformation restoreRouteInformation(RouteMatchList configuration) {
if (configuration.matches.last is ImperativeRouteMatch) {
configuration =
(configuration.matches.last as ImperativeRouteMatch).matches;
}
return RouteInformation(
location: configuration.uri.toString(),
state: configuration.extra,

View File

@ -47,10 +47,51 @@ RegExp patternToRegExp(String pattern, List<String> parameters) {
return RegExp(buffer.toString(), caseSensitive: false);
}
String _escapeGroup(String group, String name) {
/// Removes string from the end of the path that matches a `pattern`.
///
/// The path parameters can be specified by prefixing them with `:`. The
/// `parameters` are used for storing path parameter names.
///
///
/// For example:
///
/// `path` = `/user/123/book/345`
/// `pattern` = `book/:id`
///
/// The return value = `/user/123`.
String removePatternFromPath(String pattern, String path) {
final StringBuffer buffer = StringBuffer();
int start = 0;
for (final RegExpMatch match in _parameterRegExp.allMatches(pattern)) {
if (match.start > start) {
buffer.write(RegExp.escape(pattern.substring(start, match.start)));
}
final String? optionalPattern = match[2];
final String regex =
optionalPattern != null ? _escapeGroup(optionalPattern) : '[^/]+';
buffer.write(regex);
start = match.end;
}
if (start < pattern.length) {
buffer.write(RegExp.escape(pattern.substring(start)));
}
if (!pattern.endsWith('/')) {
buffer.write(r'(?=/|$)');
}
buffer.write(r'$');
final RegExp regexp = RegExp(buffer.toString(), caseSensitive: false);
return path.replaceFirst(regexp, '');
}
String _escapeGroup(String group, [String? name]) {
final String escapedGroup = group.replaceFirstMapped(
RegExp(r'[:=!]'), (Match match) => '\\${match[0]}');
return '(?<$name>$escapedGroup)';
if (name != null) {
return '(?<$name>$escapedGroup)';
}
return escapedGroup;
}
/// Reconstructs the full path from a [pattern] and path parameters.

View File

@ -300,6 +300,7 @@ class GoRoute extends RouteBase {
/// Navigator instead of the nearest ShellRoute ancestor.
final GlobalKey<NavigatorState>? parentNavigatorKey;
// TODO(chunhtai): move all regex related help methods to path_utils.dart.
/// Match this route against a location.
RegExpMatch? matchPatternAsPrefix(String loc) =>
_pathRE.matchAsPrefix(loc) as RegExpMatch?;

View File

@ -1,7 +1,7 @@
name: go_router
description: A declarative router for Flutter based on Navigation 2 supporting
deep linking, data-driven routes and more
version: 5.2.1
version: 5.2.2
repository: https://github.com/flutter/packages/tree/main/packages/go_router
issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+go_router%22

View File

@ -879,6 +879,138 @@ void main() {
});
});
group('report correct url', () {
final List<MethodCall> log = <MethodCall>[];
setUp(() {
TestDefaultBinaryMessengerBinding.instance!.defaultBinaryMessenger
.setMockMethodCallHandler(SystemChannels.navigation,
(MethodCall methodCall) async {
log.add(methodCall);
return null;
});
});
tearDown(() {
TestDefaultBinaryMessengerBinding.instance!.defaultBinaryMessenger
.setMockMethodCallHandler(SystemChannels.navigation, null);
log.clear();
});
testWidgets('on push', (WidgetTester tester) async {
final List<GoRoute> routes = <GoRoute>[
GoRoute(
path: '/',
builder: (_, __) => const DummyScreen(),
),
GoRoute(
path: '/settings',
builder: (_, __) => const DummyScreen(),
),
];
final GoRouter router = await createRouter(routes, tester);
log.clear();
router.push('/settings');
await tester.pumpAndSettle();
expect(log, <Object>[
isMethodCall('selectMultiEntryHistory', arguments: null),
isMethodCall('routeInformationUpdated', arguments: <String, dynamic>{
'location': '/settings',
'state': null,
'replace': false
}),
]);
});
testWidgets('on pop', (WidgetTester tester) async {
final List<GoRoute> routes = <GoRoute>[
GoRoute(
path: '/',
builder: (_, __) => const DummyScreen(),
routes: <RouteBase>[
GoRoute(
path: 'settings',
builder: (_, __) => const DummyScreen(),
),
]),
];
final GoRouter router =
await createRouter(routes, tester, initialLocation: '/settings');
log.clear();
router.pop();
await tester.pumpAndSettle();
expect(log, <Object>[
isMethodCall('selectMultiEntryHistory', arguments: null),
isMethodCall('routeInformationUpdated', arguments: <String, dynamic>{
'location': '/',
'state': null,
'replace': false
}),
]);
});
testWidgets('on pop with path parameters', (WidgetTester tester) async {
final List<GoRoute> routes = <GoRoute>[
GoRoute(
path: '/',
builder: (_, __) => const DummyScreen(),
routes: <RouteBase>[
GoRoute(
path: 'settings/:id',
builder: (_, __) => const DummyScreen(),
),
]),
];
final GoRouter router =
await createRouter(routes, tester, initialLocation: '/settings/123');
log.clear();
router.pop();
await tester.pumpAndSettle();
expect(log, <Object>[
isMethodCall('selectMultiEntryHistory', arguments: null),
isMethodCall('routeInformationUpdated', arguments: <String, dynamic>{
'location': '/',
'state': null,
'replace': false
}),
]);
});
testWidgets('on pop with path parameters case 2',
(WidgetTester tester) async {
final List<GoRoute> routes = <GoRoute>[
GoRoute(
path: '/',
builder: (_, __) => const DummyScreen(),
routes: <RouteBase>[
GoRoute(
path: ':id',
builder: (_, __) => const DummyScreen(),
),
]),
];
final GoRouter router =
await createRouter(routes, tester, initialLocation: '/123/');
log.clear();
router.pop();
await tester.pumpAndSettle();
expect(log, <Object>[
isMethodCall('selectMultiEntryHistory', arguments: null),
isMethodCall('routeInformationUpdated', arguments: <String, dynamic>{
'location': '/',
'state': null,
'replace': false
}),
]);
});
});
group('named routes', () {
testWidgets('match home route', (WidgetTester tester) async {
final List<GoRoute> routes = <GoRoute>[