Skip to content
Open
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
4 changes: 4 additions & 0 deletions packages/go_router/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
## 17.1.1

- Fixes `pop()` restoring stale configuration when route has `onExit`, which could cause the popped route to reappear with async redirects.

## 17.1.0

- Adds `TypedQueryParameter` annotation to override parameter names in `TypedGoRoute` constructors.
Expand Down
7 changes: 6 additions & 1 deletion packages/go_router/lib/src/router.dart
Original file line number Diff line number Diff line change
Expand Up @@ -583,8 +583,13 @@ class GoRouter implements RouterConfig<RouteMatchList> {
log('popping ${routerDelegate.currentConfiguration.uri}');
return true;
}());
final RouteMatchList configBeforePop = routerDelegate.currentConfiguration;
routerDelegate.pop<T>(result);
restore(routerDelegate.currentConfiguration);
// Only restore when the pop completed synchronously (no onExit).
// If deferred, currentConfiguration is still the same instance.
Comment on lines +588 to +589

Choose a reason for hiding this comment

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

medium

The current comment explains the condition well. To further improve maintainability, consider adding a brief note about why this check is important, mentioning the risk of restoring a stale configuration. This will help future developers understand the full context and prevent accidental regressions.

// Only restore on a synchronous pop to avoid restoring a stale configuration.
// A deferred pop (e.g. due to an async `onExit`) would leave the
// `currentConfiguration` instance unchanged, and restoring it could cause issues.

if (!identical(routerDelegate.currentConfiguration, configBeforePop)) {
restore(routerDelegate.currentConfiguration);
}
}

/// Refresh the route.
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: 17.1.0
version: 17.1.1
repository: https://github.com/flutter/packages/tree/main/packages/go_router
issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+go_router%22

Expand Down
85 changes: 85 additions & 0 deletions packages/go_router/test/on_exit_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -493,4 +493,89 @@ void main() {
expect(onExitState2.fullPath, '/route-2/:id2');
},
);

// Regression test: pop() with onExit + async redirect must not restore
// stale configuration.
testWidgets(
'pop does not call restore with stale config when route has onExit',
(WidgetTester tester) async {
final homeKey = UniqueKey();
final detailKey = UniqueKey();

final GoRouter router = await createRouter(
<RouteBase>[
GoRoute(
path: '/',
builder: (_, __) => DummyScreen(key: homeKey),
routes: <RouteBase>[
GoRoute(
path: 'detail',
onExit: (_, __) => true,
builder: (_, __) => DummyScreen(key: detailKey),
),
],
),
],
tester,
initialLocation: '/detail',
redirect: (_, GoRouterState state) async {
// Async redirect — completes in a later microtask.
await Future<void>.delayed(Duration.zero);
return null;
},
);

await tester.pumpAndSettle();
expect(find.byKey(detailKey), findsOneWidget);

router.pop();
await tester.pumpAndSettle();

// The detail route should be gone after pop.
expect(
find.byKey(detailKey),
findsNothing,
reason:
'Route with onExit should be properly popped '
'even when async redirect is present',
);
expect(find.byKey(homeKey), findsOneWidget);
},
);

// Verify that pop is correctly cancelled when onExit returns false.
testWidgets('pop is cancelled when onExit returns false', (
WidgetTester tester,
) async {
final homeKey = UniqueKey();
final detailKey = UniqueKey();

final GoRouter router = await createRouter(
<RouteBase>[
GoRoute(
path: '/',
builder: (_, __) => DummyScreen(key: homeKey),
routes: <RouteBase>[
GoRoute(
path: 'detail',
onExit: (_, __) => false, // Always prevent leaving.
builder: (_, __) => DummyScreen(key: detailKey),
),
],
),
],
tester,
initialLocation: '/detail',
);

await tester.pumpAndSettle();
expect(find.byKey(detailKey), findsOneWidget);

router.pop();
await tester.pumpAndSettle();

// Should still be on the detail page.
expect(find.byKey(detailKey), findsOneWidget);
expect(find.byKey(homeKey), findsNothing);
});
}
Loading