diff --git a/packages/go_router/CHANGELOG.md b/packages/go_router/CHANGELOG.md index 79eb13d61fad..fd6ba56e98e7 100644 --- a/packages/go_router/CHANGELOG.md +++ b/packages/go_router/CHANGELOG.md @@ -1,3 +1,10 @@ +## 17.3.1 + +- Fixes a "Bad state: Future already completed" crash when an imperatively-pushed + route's completion is triggered more than once (for example a rapid double-pop). + `ImperativeRouteMatch.complete` is now idempotent. See + https://github.com/flutter/flutter/issues/156809. + ## 17.3.0 - Updates minimum supported SDK version to Flutter 3.38/Dart 3.10. diff --git a/packages/go_router/lib/src/match.dart b/packages/go_router/lib/src/match.dart index b66edab0f4c1..fbddf252e550 100644 --- a/packages/go_router/lib/src/match.dart +++ b/packages/go_router/lib/src/match.dart @@ -444,6 +444,15 @@ class ImperativeRouteMatch extends RouteMatch { /// Called when the corresponding [Route] associated with this route match is /// completed. void complete([dynamic value]) { + // Completing is idempotent. The pushed route can be removed more than once + // — e.g. a rapid double-pop, or a pop dispatched while a prior removal is + // still in flight on a slow device — which would otherwise call complete() + // a second time and throw "Bad state: Future already completed". The + // awaited result is delivered by the first completion, so the duplicate is + // a no-op. See https://github.com/flutter/flutter/issues/156809. + if (completer.isCompleted) { + return; + } completer.complete(value); } diff --git a/packages/go_router/pubspec.yaml b/packages/go_router/pubspec.yaml index 77e1c11fde77..002dada9f39d 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.3.0 +version: 17.3.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/match_test.dart b/packages/go_router/test/match_test.dart index 7a1fcd956f62..33599615adf3 100644 --- a/packages/go_router/test/match_test.dart +++ b/packages/go_router/test/match_test.dart @@ -178,6 +178,20 @@ void main() { final completer1 = Completer(); final completer2 = Completer(); + test('complete is idempotent and does not throw when called twice', () async { + // Regression test for https://github.com/flutter/flutter/issues/156809: + // the pushed route's completer could be completed more than once + // (e.g. a rapid double-pop), throwing "Bad state: Future already + // completed". + final completer = Completer(); + final match = ImperativeRouteMatch(pageKey: key1, matches: matchList1, completer: completer); + + match.complete('result'); + // A second completion must be a no-op rather than a crash. + expect(() => match.complete('ignored'), returnsNormally); + expect(await completer.future, 'result'); + }); + test('can equal and has', () async { var match1 = ImperativeRouteMatch(pageKey: key1, matches: matchList1, completer: completer1); var match2 = ImperativeRouteMatch(pageKey: key1, matches: matchList1, completer: completer1);