Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions packages/go_router/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
## 6.0.1

- Fixes crashes when popping navigators manually.
- Fixes trailing slashes after pops.

## 6.0.0

- **BREAKING CHANGE**
Expand Down
32 changes: 26 additions & 6 deletions packages/go_router/lib/src/builder.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'package:collection/collection.dart';
import 'package:flutter/widgets.dart';

import 'configuration.dart';
Expand Down Expand Up @@ -50,13 +51,26 @@ class RouteBuilder {

final GoRouterStateRegistry _registry = GoRouterStateRegistry();

final Map<Page<Object?>, RouteMatch> _routeMatchLookUp =
<Page<Object?>, RouteMatch>{};

/// Looks the the [RouteMatch] for a given [Page].
///
/// The [Page] must be in the latest [Navigator.pages]; otherwise, this method
/// returns null.
RouteMatch? getRouteMatchForPage(Page<Object?> page) =>
_routeMatchLookUp[page];

// final Map<>

/// Builds the top-level Navigator for the given [RouteMatchList].
Widget build(
BuildContext context,
RouteMatchList matchList,
PopPageCallback onPopPage,
bool routerNeglect,
) {
_routeMatchLookUp.clear();
if (matchList.isEmpty) {
// The build method can be called before async redirect finishes. Build a
// empty box until then.
Expand Down Expand Up @@ -119,10 +133,15 @@ class RouteBuilder {
GlobalKey<NavigatorState> navigatorKey,
Map<Page<Object?>, GoRouterState> registry) {
try {
assert(_routeMatchLookUp.isEmpty);
final Map<GlobalKey<NavigatorState>, List<Page<Object?>>> keyToPage =
<GlobalKey<NavigatorState>, List<Page<Object?>>>{};
_buildRecursive(context, matchList, 0, onPopPage, routerNeglect,
keyToPage, navigatorKey, registry);

// Every Page should have a corresponding RouteMatch.
assert(keyToPage.values.flattened
.every((Page<Object?> page) => _routeMatchLookUp.containsKey(page)));
return keyToPage[navigatorKey]!;
} on _RouteBuilderError catch (e) {
return <Page<Object?>>[
Expand Down Expand Up @@ -271,12 +290,13 @@ class RouteBuilder {
page = null;
}

page ??= buildPage(context, state, Builder(builder: (BuildContext context) {
return _callRouteBuilder(context, state, match, childWidget: child);
}));
_routeMatchLookUp[page] = match;

// Return the result of the route's builder() or pageBuilder()
return page ??
// Uses a Builder to make sure its rebuild scope is limited to the page.
buildPage(context, state, Builder(builder: (BuildContext context) {
return _callRouteBuilder(context, state, match, childWidget: child);
}));
return page;
}

/// Calls the user-provided route builder from the [RouteMatch]'s [RouteBase].
Expand Down Expand Up @@ -363,7 +383,7 @@ class RouteBuilder {
_cacheAppType(context);
return _pageBuilderForAppType!(
key: state.pageKey,
name: state.name ?? state.fullpath,
name: state.name ?? state.path,
arguments: <String, String>{...state.params, ...state.queryParams},
restorationId: state.pageKey.value,
child: child,
Expand Down
12 changes: 8 additions & 4 deletions packages/go_router/lib/src/delegate.dart
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import 'typedefs.dart';

/// GoRouter implementation of [RouterDelegate].
class GoRouterDelegate extends RouterDelegate<RouteMatchList>
with PopNavigatorRouterDelegateMixin<RouteMatchList>, ChangeNotifier {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We missed this when we refactor the pop. PopNavigatorRouterDelegateMixin is no longer needed since we override poproute directly

with ChangeNotifier {
/// Constructor for GoRouter's implementation of the RouterDelegate base
/// class.
GoRouterDelegate({
Expand Down Expand Up @@ -132,7 +132,12 @@ class GoRouterDelegate extends RouterDelegate<RouteMatchList>
if (!route.didPop(result)) {
return false;
}
_matchList.pop();
final Page<Object?> page = route.settings as Page<Object?>;
final RouteMatch? match = builder.getRouteMatchForPage(page);
if (match == null) {
return true;
}
_matchList.remove(match);
notifyListeners();
assert(() {
_debugAssertMatchListNotEmpty();
Expand All @@ -146,7 +151,7 @@ class GoRouterDelegate extends RouterDelegate<RouteMatchList>
/// See also:
/// * [push] which pushes the given location onto the page stack.
void pushReplacement(RouteMatchList matches) {
_matchList.pop();
_matchList.remove(_matchList.last);
push(matches); // [push] will notify the listeners.
}

Expand All @@ -155,7 +160,6 @@ class GoRouterDelegate extends RouterDelegate<RouteMatchList>
RouteMatchList get matches => _matchList;

/// For use by the Router architecture as part of the RouterDelegate.
@override
GlobalKey<NavigatorState> get navigatorKey => _configuration.navigatorKey;

/// For use by the Router architecture as part of the RouterDelegate.
Expand Down
2 changes: 1 addition & 1 deletion packages/go_router/lib/src/match.dart
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,6 @@ class RouteMatch {
/// An exception if there was an error during matching.
final Exception? error;

/// Optional value key of type string, to hold a unique reference to a page.
/// Value key of type string, to hold a unique reference to a page.
final ValueKey<String> pageKey;
}
27 changes: 19 additions & 8 deletions packages/go_router/lib/src/matching.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import 'package:flutter/widgets.dart';

import 'configuration.dart';
import 'delegate.dart';
import 'match.dart';
import 'path_utils.dart';

Expand Down Expand Up @@ -56,7 +57,7 @@ class RouteMatchList {
static RouteMatchList empty =
RouteMatchList(<RouteMatch>[], Uri.parse(''), const <String, String>{});

static String _generateFullPath(List<RouteMatch> matches) {
static String _generateFullPath(Iterable<RouteMatch> matches) {
final StringBuffer buffer = StringBuffer();
bool addsSlash = false;
for (final RouteMatch match in matches) {
Expand Down Expand Up @@ -96,17 +97,27 @@ class RouteMatchList {
_matches.add(match);
}

/// 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();
/// Removes the match from the list.
void remove(RouteMatch match) {
final int index = _matches.indexOf(match);
assert(index != -1);
_matches.removeRange(index, _matches.length);

// Also pop ShellRoutes when there are no subsequent route matches
while (_matches.isNotEmpty && _matches.last.route is ShellRoute) {
_matches.removeLast();
}

final String fullPath = _generateFullPath(
_matches.where((RouteMatch match) => match is! ImperativeRouteMatch));
// Need to remove path parameters that are no longer in the fullPath.
final List<String> newParameters = <String>[];
patternToRegExp(fullPath, newParameters);
final Set<String> validParameters = newParameters.toSet();
pathParameters.removeWhere(
(String key, String value) => !validParameters.contains(key));

_uri = _uri.replace(path: patternToPath(fullPath, pathParameters));
}

/// An optional object provided by the app during navigation.
Expand Down
38 changes: 0 additions & 38 deletions packages/go_router/lib/src/path_utils.dart
Original file line number Diff line number Diff line change
Expand Up @@ -47,44 +47,6 @@ RegExp patternToRegExp(String pattern, List<String> parameters) {
return RegExp(buffer.toString(), caseSensitive: false);
}

/// 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]}');
Expand Down
2 changes: 1 addition & 1 deletion packages/go_router/pubspec.yaml
Original file line number Diff line number Diff line change
@@ -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: 6.0.0
version: 6.0.1
repository: https://github.com/flutter/packages/tree/main/packages/go_router
issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+go_router%22

Expand Down
109 changes: 109 additions & 0 deletions packages/go_router/test/go_router_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -953,6 +953,41 @@ void main() {
]);
});

testWidgets('on pop twice', (WidgetTester tester) async {
final List<GoRoute> routes = <GoRoute>[
GoRoute(
path: '/',
builder: (_, __) => const DummyScreen(),
routes: <RouteBase>[
GoRoute(
path: 'settings',
builder: (_, __) => const DummyScreen(),
routes: <RouteBase>[
GoRoute(
path: 'profile',
builder: (_, __) => const DummyScreen(),
),
]),
]),
];

final GoRouter router = await createRouter(routes, tester,
initialLocation: '/settings/profile');

log.clear();
router.pop();
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(
Expand Down Expand Up @@ -1012,6 +1047,80 @@ void main() {
]);
});

testWidgets('Can manually pop root navigator and display correct url',
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we also add a test case that pop multiple times?
I think this PR would fix flutter/flutter#117431 but just want to make sure of it.

I added a test case for it in this PR: #2989 but since it's close, I'd like to add it in this PR at least.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

added on pop twice

Copy link

@leehack leehack Dec 21, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks ;)
I have no right to approve the PR but everything looks good to me.

(WidgetTester tester) async {
final GlobalKey<NavigatorState> rootNavigatorKey =
GlobalKey<NavigatorState>();

final List<RouteBase> routes = <RouteBase>[
GoRoute(
path: '/',
builder: (BuildContext context, GoRouterState state) {
return const Scaffold(
body: Text('Home'),
);
},
routes: <RouteBase>[
ShellRoute(
builder:
(BuildContext context, GoRouterState state, Widget child) {
return Scaffold(
appBar: AppBar(),
body: child,
);
},
routes: <RouteBase>[
GoRoute(
path: 'b',
builder: (BuildContext context, GoRouterState state) {
return const Scaffold(
body: Text('Screen B'),
);
},
routes: <RouteBase>[
GoRoute(
path: 'c',
builder: (BuildContext context, GoRouterState state) {
return const Scaffold(
body: Text('Screen C'),
);
},
),
],
),
],
),
],
),
];

await createRouter(routes, tester,
initialLocation: '/b/c', navigatorKey: rootNavigatorKey);
expect(find.text('Screen C'), findsOneWidget);
expect(log, <Object>[
isMethodCall('selectMultiEntryHistory', arguments: null),
isMethodCall('routeInformationUpdated', arguments: <String, dynamic>{
'location': '/b/c',
'state': null,
'replace': false
}),
]);

log.clear();
rootNavigatorKey.currentState!.pop();
await tester.pumpAndSettle();

expect(find.text('Home'), findsOneWidget);
expect(log, <Object>[
isMethodCall('selectMultiEntryHistory', arguments: null),
isMethodCall('routeInformationUpdated', arguments: <String, dynamic>{
'location': '/',
'state': null,
'replace': false
}),
]);
});

testWidgets('works correctly with async redirect',
(WidgetTester tester) async {
final UniqueKey login = UniqueKey();
Expand Down