mirror of
https://github.com/flutter/packages.git
synced 2025-07-03 00:49:32 +08:00
[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:
@ -1,3 +1,7 @@
|
|||||||
|
## 5.2.2
|
||||||
|
|
||||||
|
- Fixes `pop` and `push` to update urls correctly.
|
||||||
|
|
||||||
## 5.2.1
|
## 5.2.1
|
||||||
|
|
||||||
- Refactors `GoRouter.pop` to be able to pop individual pageless route with result.
|
- Refactors `GoRouter.pop` to be able to pop individual pageless route with result.
|
||||||
|
@ -133,6 +133,7 @@ class GoRouterDelegate extends RouterDelegate<RouteMatchList>
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
_matchList.pop();
|
_matchList.pop();
|
||||||
|
notifyListeners();
|
||||||
assert(() {
|
assert(() {
|
||||||
_debugAssertMatchListNotEmpty();
|
_debugAssertMatchListNotEmpty();
|
||||||
return true;
|
return true;
|
||||||
|
@ -48,7 +48,7 @@ class RouteMatcher {
|
|||||||
/// The list of [RouteMatch] objects.
|
/// The list of [RouteMatch] objects.
|
||||||
class RouteMatchList {
|
class RouteMatchList {
|
||||||
/// RouteMatchList constructor.
|
/// RouteMatchList constructor.
|
||||||
RouteMatchList(List<RouteMatch> matches, this.uri, this.pathParameters)
|
RouteMatchList(List<RouteMatch> matches, this._uri, this.pathParameters)
|
||||||
: _matches = matches,
|
: _matches = matches,
|
||||||
fullpath = _generateFullPath(matches);
|
fullpath = _generateFullPath(matches);
|
||||||
|
|
||||||
@ -82,7 +82,8 @@ class RouteMatchList {
|
|||||||
final Map<String, String> pathParameters;
|
final Map<String, String> pathParameters;
|
||||||
|
|
||||||
/// The uri of the current match.
|
/// The uri of the current match.
|
||||||
final Uri uri;
|
Uri get uri => _uri;
|
||||||
|
Uri _uri;
|
||||||
|
|
||||||
/// Returns true if there are no matches.
|
/// Returns true if there are no matches.
|
||||||
bool get isEmpty => _matches.isEmpty;
|
bool get isEmpty => _matches.isEmpty;
|
||||||
@ -97,8 +98,11 @@ class RouteMatchList {
|
|||||||
|
|
||||||
/// Removes the last match.
|
/// Removes the last match.
|
||||||
void pop() {
|
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();
|
_matches.removeLast();
|
||||||
|
|
||||||
// Also pop ShellRoutes when there are no subsequent route matches
|
// Also pop ShellRoutes when there are no subsequent route matches
|
||||||
while (_matches.isNotEmpty && _matches.last.route is ShellRoute) {
|
while (_matches.isNotEmpty && _matches.last.route is ShellRoute) {
|
||||||
_matches.removeLast();
|
_matches.removeLast();
|
||||||
|
@ -8,6 +8,7 @@ import 'package:flutter/foundation.dart';
|
|||||||
import 'package:flutter/widgets.dart';
|
import 'package:flutter/widgets.dart';
|
||||||
|
|
||||||
import 'configuration.dart';
|
import 'configuration.dart';
|
||||||
|
import 'delegate.dart';
|
||||||
import 'information_provider.dart';
|
import 'information_provider.dart';
|
||||||
import 'logging.dart';
|
import 'logging.dart';
|
||||||
import 'matching.dart';
|
import 'matching.dart';
|
||||||
@ -98,6 +99,10 @@ class GoRouteInformationParser extends RouteInformationParser<RouteMatchList> {
|
|||||||
/// 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(RouteMatchList configuration) {
|
RouteInformation restoreRouteInformation(RouteMatchList configuration) {
|
||||||
|
if (configuration.matches.last is ImperativeRouteMatch) {
|
||||||
|
configuration =
|
||||||
|
(configuration.matches.last as ImperativeRouteMatch).matches;
|
||||||
|
}
|
||||||
return RouteInformation(
|
return RouteInformation(
|
||||||
location: configuration.uri.toString(),
|
location: configuration.uri.toString(),
|
||||||
state: configuration.extra,
|
state: configuration.extra,
|
||||||
|
@ -47,10 +47,51 @@ RegExp patternToRegExp(String pattern, List<String> parameters) {
|
|||||||
return RegExp(buffer.toString(), caseSensitive: false);
|
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(
|
final String escapedGroup = group.replaceFirstMapped(
|
||||||
RegExp(r'[:=!]'), (Match match) => '\\${match[0]}');
|
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.
|
/// Reconstructs the full path from a [pattern] and path parameters.
|
||||||
|
@ -300,6 +300,7 @@ class GoRoute extends RouteBase {
|
|||||||
/// Navigator instead of the nearest ShellRoute ancestor.
|
/// Navigator instead of the nearest ShellRoute ancestor.
|
||||||
final GlobalKey<NavigatorState>? parentNavigatorKey;
|
final GlobalKey<NavigatorState>? parentNavigatorKey;
|
||||||
|
|
||||||
|
// TODO(chunhtai): move all regex related help methods to path_utils.dart.
|
||||||
/// Match this route against a location.
|
/// Match this route against a location.
|
||||||
RegExpMatch? matchPatternAsPrefix(String loc) =>
|
RegExpMatch? matchPatternAsPrefix(String loc) =>
|
||||||
_pathRE.matchAsPrefix(loc) as RegExpMatch?;
|
_pathRE.matchAsPrefix(loc) as RegExpMatch?;
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
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: 5.2.1
|
version: 5.2.2
|
||||||
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
|
||||||
|
|
||||||
|
@ -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', () {
|
group('named routes', () {
|
||||||
testWidgets('match home route', (WidgetTester tester) async {
|
testWidgets('match home route', (WidgetTester tester) async {
|
||||||
final List<GoRoute> routes = <GoRoute>[
|
final List<GoRoute> routes = <GoRoute>[
|
||||||
|
Reference in New Issue
Block a user