[go_router] Fix spurious onExit when refreshListenable re-parses a pushed route#12212
Open
davidmigloz wants to merge 1 commit into
Open
[go_router] Fix spurious onExit when refreshListenable re-parses a pushed route#12212davidmigloz wants to merge 1 commit into
davidmigloz wants to merge 1 commit into
Conversation
Contributor
There was a problem hiding this comment.
Code Review
This pull request excludes the ephemeral completer from the ImperativeRouteMatch equality (==) and hashCode calculations to prevent spurious onExit calls when a route is re-decoded (e.g., during a refreshListenable tick). It also adds corresponding unit and widget tests to verify this behavior. One review comment suggests wrapping an unawaited router.push call in unawaited to satisfy the unawaited_futures lint rule.
…s onExit A `refreshListenable` tick (e.g. tab refocus) re-parses the encoded current configuration. The codec mints a fresh `Completer` per `ImperativeRouteMatch`, so the re-decoded list compares unequal to the live one, the delegate's no-op fast path is defeated, and `onExit` fires on a route that never left the stack. Instead of dropping the completer from `ImperativeRouteMatch` identity — which breaks `replace()` to the same location (it reuses the old pageKey and differs only by its new completer, so the new configuration would compare equal, the `setNewRoutePath` fast path would no-op, and the returned future would be orphaned) — reconcile completers only on the re-decode path: for each decoded imperative match, adopt the live completer of the match with the same pageKey (unique per push). The re-decoded list then compares equal including completer, the fast path holds naturally, and in-flight push futures are preserved. Tests: onExit not fired on a refresh tick; live push completer resolves across a refresh; replace() to the same location still completes its future; reconcileImperativeCompleters adopts live completers by pageKey and keeps fresh ones when there is no live match.
davidmigloz
force-pushed
the
upstream-imperative-match-completer-identity
branch
from
July 16, 2026 14:07
7133569 to
4756afd
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
GoRoute.onExitfires spuriously for a pushed route that never left the navigation stack wheneverrefreshListenablefires (auth refresh, a tab-refocus listener) while that route is on top. If the route usesonExitfor a "discard unsaved changes?" prompt, the user sees it on every refresh tick.Root cause:
ImperativeRouteMatch.completeris the ephemeral result channel forpush(...).then(...)and can't survive a codec round-trip — arefreshListenablere-parse (and state restoration) decodes a brand-newCompleter. The re-decoded match list therefore compares unequal to the live one even though nothing changed, defeatingsetNewRoutePath's no-op fast path; the router treats the re-parse as a real navigation change and runsonExit.Fix: on the re-decode path only (the parser branch handling restoration, browser history, and refresh re-parses), carry over the live completer for each imperative match by
pageKey(unique per push) via an internalRouteMatchList.reconcileImperativeCompleters. The re-decoded configuration then compares equal to the live one — completer included — so the fast path holds,onExitdoesn't fire, and in-flightpushfutures are preserved.operator==/hashCodeare untouched, and imperative navigations (push/replace/pushReplacement) never hit this path.An earlier version of this PR excluded the completer from
ImperativeRouteMatchequality instead. That regressed same-locationreplace()— whose match deliberately reusespageKeyand differs only by its new completer — orphaning the returnedFuture; see the PR thread for details.Tests: no spurious
onExiton a refresh tick; a livepushfuture resolves across the tick; same-locationreplace()still completes its future; reconciliation contract tests inmatch_test.dart. Full go_router suite and analyzer green.Fixes flutter/flutter#189578
Pre-Review Checklist
[shared_preferences]///).Footnotes
Regular contributors who have demonstrated familiarity with the repository guidelines only need to comment if the PR is not auto-exempted by repo tooling. ↩ ↩2