From 5276a8a09c77a40bb44695d08508ae01176c5734 Mon Sep 17 00:00:00 2001 From: iCodePoet Date: Wed, 19 Aug 2026 02:41:43 +0900 Subject: [PATCH] [go_router] Fix "Bad state: Future already completed" when a route with onExit is popped twice --- packages/go_router/CHANGELOG.md | 4 +++ packages/go_router/lib/src/match.dart | 8 +++++ packages/go_router/pubspec.yaml | 2 +- packages/go_router/test/on_exit_test.dart | 39 +++++++++++++++++++++++ 4 files changed, 52 insertions(+), 1 deletion(-) diff --git a/packages/go_router/CHANGELOG.md b/packages/go_router/CHANGELOG.md index ef05a190eb98..a05a51aaa089 100644 --- a/packages/go_router/CHANGELOG.md +++ b/packages/go_router/CHANGELOG.md @@ -1,3 +1,7 @@ +## 17.5.1 + +- Fixes a `StateError` ("Bad state: Future already completed") when a route with an `onExit` callback is popped twice before the navigator rebuilds. + ## 17.5.0 - Adds route `metadata` support, including inheritance and override behavior with exposure on `GoRouterState`. diff --git a/packages/go_router/lib/src/match.dart b/packages/go_router/lib/src/match.dart index bc7de56d39fa..8d0b27336ebd 100644 --- a/packages/go_router/lib/src/match.dart +++ b/packages/go_router/lib/src/match.dart @@ -458,7 +458,15 @@ class ImperativeRouteMatch extends RouteMatch { /// Called when the corresponding [Route] associated with this route match is /// completed. + /// + /// A route match can be completed more than once when the same route is + /// popped again before the navigator has rebuilt; for example, two back + /// events in quick succession while an `onExit` callback defers the pop to a + /// microtask. Only the first completion is reported; later ones are ignored. void complete([dynamic value]) { + if (completer.isCompleted) { + return; + } completer.complete(value); } diff --git a/packages/go_router/pubspec.yaml b/packages/go_router/pubspec.yaml index d8641367c8d9..fc2eac3ae1e2 100644 --- a/packages/go_router/pubspec.yaml +++ b/packages/go_router/pubspec.yaml @@ -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.5.0 +version: 17.5.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 diff --git a/packages/go_router/test/on_exit_test.dart b/packages/go_router/test/on_exit_test.dart index 990ee9a9a6a3..b068adaac2e3 100644 --- a/packages/go_router/test/on_exit_test.dart +++ b/packages/go_router/test/on_exit_test.dart @@ -529,6 +529,45 @@ void main() { expect(find.byKey(homeKey), findsNothing); }); + // Regression test for https://github.com/flutter/flutter/issues/191280 + testWidgets('popping a route with onExit twice before the navigator rebuilds does not throw', ( + 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, + ); + + final Future pushFuture = router.push('/detail'); + await tester.pumpAndSettle(); + expect(find.byKey(detailKey), findsOneWidget); + + // Two back events arrive before the deferred pop is applied — e.g. a user + // tapping the back button twice in quick succession. + router.pop('first'); + router.pop('second'); + await tester.pumpAndSettle(); + + expect(tester.takeException(), isNull); + expect(find.byKey(homeKey), findsOneWidget); + expect(await pushFuture, 'first'); + }); + // Regression test for https://github.com/flutter/flutter/issues/137829 testWidgets('back button works synchronously with ShellRoute', (WidgetTester tester) async { var allow = false;