[go_router] Make ImperativeRouteMatch.complete idempotent#11805
[go_router] Make ImperativeRouteMatch.complete idempotent#11805ZephyrVentum wants to merge 1 commit into
Conversation
Adds an `isCompleted` guard inside `ImperativeRouteMatch.complete` so
calling it a second time after the future has already resolved is
silently ignored instead of throwing
`Bad state: Future already completed`.
`GoRouteData` ships a default `FutureOr<bool> onExit(...) => true`,
and `go_router_builder` wires it into every generated `$route`
factory, so `routeBase.onExit != null` is true for every route
declared via `@TypedGoRoute` / `GoRouteData`. That forces every pop
through the `scheduleMicrotask` path in
`GoRouterDelegate._handlePopPageWithRouteMatch`:
scheduleMicrotask(() async {
final bool onExitResult = await routeBase.onExit!(...);
if (onExitResult) {
_completeRouteMatch(result, match);
}
});
return false;
If the framework reaches `_handlePopPageWithRouteMatch` a second
time for the same `ImperativeRouteMatch` before either microtask
runs (iOS interactive back-edge gesture firing `Navigator.onPopPage`
during the commit/animation window, chained `context.pop(result)`
calls for nested modals, etc.), both microtasks call
`_completeRouteMatch`. The first one completes the Completer and
removes the match from `currentConfiguration`; the second one calls
`completer.complete(value)` against an already-completed Completer
and throws.
The pop itself succeeds — `currentConfiguration` is already updated
and `notifyListeners` rebuilds the Navigator without the popped
page before the second microtask fires — so user-visible behaviour
is unchanged. The error only shows up in apps' global error
handlers (`PlatformDispatcher.onError` / `FlutterError.onError`),
typically forwarded to crash reporting as fatal.
Issue: flutter/flutter#187326 (also
covers the chained-pop / async-modal flavors of the same defect
previously filed as #160696, #176493, #163813, #146938, #123369).
Test: `match_test.dart` now asserts that calling `complete` twice
does not throw and the second call is a no-op.
|
Thanks for your pull request! It looks like this may be your first contribution to a Google open source project. Before we can look at your pull request, you'll need to sign a Contributor License Agreement (CLA). View this failed invocation of the CLA check for more information. For the most up to date status, view the checks section at the bottom of the pull request. |
There was a problem hiding this comment.
Code Review
This pull request updates the go_router package to version 17.2.4. It modifies the complete method in ImperativeRouteMatch to be idempotent by checking if the completer is already completed before calling complete again, preventing Bad state: Future already completed errors. A regression test has been added to verify this behavior, and the CHANGELOG has been updated. I have no feedback to provide.
Description
Adds an
isCompletedguard insideImperativeRouteMatch.completeso calling it a second time after the future has already resolved is silently ignored instead of throwingBad state: Future already completed.Root cause
GoRouteDataships a defaultFutureOr<bool> onExit(...) => true, andgo_router_builderwires it into every generated$routefactory. That makesrouteBase.onExit != nullfor every route declared via@TypedGoRoute/GoRouteData, which forces every pop through thescheduleMicrotaskpath inGoRouterDelegate._handlePopPageWithRouteMatch:Because the callback returns
false, the Navigator does not mark the popped page as resolved synchronously — the page is only actually removed when the microtask eventually runs_completeRouteMatch→currentConfiguration.remove(match)→notifyListeners→ Navigator rebuild without the page.If the framework reaches
_handlePopPageWithRouteMatcha second time for the sameImperativeRouteMatchbefore either microtask runs, both microtasks call_completeRouteMatch. The first one completes the Completer and removes the match; the second one callscompleter.complete(value)against an already-completed Completer and throws.Triggers observed in the wild include:
Navigator.onPopPagemore than once during the commit/animation window for Page-based routes.context.pop(result)calls bubbling a value through nested modals.Behavior change
User-visible behavior is unchanged either way —
currentConfigurationis already updated andnotifyListenersrebuilds the Navigator without the popped page before the second microtask fires. The error only manifests as a noisy throw in apps' global error handlers (PlatformDispatcher.onError/FlutterError.onError), typically forwarded to crash-reporting as fatal.With this patch, the second
completecall is a no-op, the original future value is preserved, and no exception is thrown.Tests
packages/go_router/test/match_test.dartgains one regression test under the existinggroup('ImperativeRouteMatch'):Verified locally:
dart analyze packages/go_router/— clean.flutter test packages/go_router/— 430 passed, 2 skipped (no regressions; new test passes).Related Issues
context.pop()during loading-dialog dismissal.ShellRoutenested route-stack.go/goNamedafterpop.context.pop()complaint.Pre-Review Checklist
[shared_preferences].pubspec.yamlwith an appropriate new version according to the pub versioning philosophy, or this PR is exempt from version changes.CHANGELOG.mdto add a description of the change, following repository CHANGELOG style, and updated or removed any TODOs.///).