diff --git a/packages/go_router/lib/src/router.dart b/packages/go_router/lib/src/router.dart index 7e885742ec52..de8bdf1b8887 100644 --- a/packages/go_router/lib/src/router.dart +++ b/packages/go_router/lib/src/router.dart @@ -583,8 +583,13 @@ class GoRouter implements RouterConfig { log('popping ${routerDelegate.currentConfiguration.uri}'); return true; }()); + final RouteMatchList configBeforePop = routerDelegate.currentConfiguration; routerDelegate.pop(result); - restore(routerDelegate.currentConfiguration); + // Only restore when the pop completed synchronously (no onExit). + // If deferred, currentConfiguration is still the same instance. + if (!identical(routerDelegate.currentConfiguration, configBeforePop)) { + restore(routerDelegate.currentConfiguration); + } } /// Refresh the route. diff --git a/packages/go_router/pending_changelogs/11241_fix_pop_restore.yaml b/packages/go_router/pending_changelogs/11241_fix_pop_restore.yaml new file mode 100644 index 000000000000..eabec681742b --- /dev/null +++ b/packages/go_router/pending_changelogs/11241_fix_pop_restore.yaml @@ -0,0 +1,3 @@ +changelog: | + - Fixes `pop()` restoring stale configuration when route has `onExit`, which could cause the popped route to reappear with async redirects. +version: patch diff --git a/packages/go_router/test/on_exit_test.dart b/packages/go_router/test/on_exit_test.dart index 2b1d0873cb95..56f5c6119698 100644 --- a/packages/go_router/test/on_exit_test.dart +++ b/packages/go_router/test/on_exit_test.dart @@ -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( + [ + GoRoute( + path: '/', + builder: (_, __) => DummyScreen(key: homeKey), + routes: [ + 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.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( + [ + GoRoute( + path: '/', + builder: (_, __) => DummyScreen(key: homeKey), + routes: [ + 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); + }); }