Repository navigation
Conversation
There was a problem hiding this comment.
Code Review
This pull request resolves key collision issues within go_router that occurred when navigating between sibling ShellRoute branches. The fix involves generating unique navigator and page keys for each ShellRouteMatch and using the navigator key object directly for _CustomNavigator's key instead of its hash code. These changes are well-tested with a new regression test that forces key hash collisions to verify the fix's robustness. The implementation is sound and effectively addresses the described problem.
| if (subRouteMatches?.isEmpty ?? true) { | ||
| return _empty; | ||
| } | ||
| final matchNavigatorKey = route is ShellRoute |
There was a problem hiding this comment.
does this mean the shell route navigator won't have stable key anymore? this may cause the navigator to keep rebuild from scratch every time the route is reparsed
| context, | ||
| _CustomNavigator( | ||
| // The state needs to persist across rebuild. | ||
| key: GlobalObjectKey(configuration.navigatorKey.hashCode), |
There was a problem hiding this comment.
just want to double check, this is purely clean up right? I would be surprised if this causes different behavior
| return Scaffold( | ||
| body: TextButton( | ||
| onPressed: () async { | ||
| shellNavigatorKey.currentState!.push( |
There was a problem hiding this comment.
This doesn't feel right. maybe we want a different way to create the persistent globalkey?
|
From triage: @Gorniv Are you planning on updating this PR based on the feedback above? |
|
@Gorniv I'm going to go ahead and close this PR as inactive, but if you end up having time to return to it, please let us know and we'll be happy to help you move forward with it. |
…#12549) This supersedes #11143, which was closed as inactive. It incorporates the review feedback from that PR. ## Problem When `push()` navigates between sibling or nested regular `ShellRoute` branches, multiple shell pages and navigators can coexist in the widget tree. This could produce duplicate page or widget keys because: - `_CustomNavigator` was keyed using `GlobalObjectKey(navigatorKey.hashCode)`, so distinct navigator keys with the same hash were treated as the same key. - `ShellRouteMatch.pageKey` was derived only from `route.hashCode`. - Re-entering a `ShellRoute` that was already present in the imperative stack reused its page and navigator identities. The first implementation in #11143 created fresh page and navigator keys for every `ShellRouteMatch`. Review correctly pointed out that this made navigator identity unstable across route reparses and prevented the configured `ShellRoute.navigatorKey` from consistently identifying its navigator. `RoutingConfig` updates could also leave inactive loaded `StatefulShellRoute` branches holding matches and navigator widgets built from the old route objects. ## Fix This version addresses that feedback by: - preserving the configured navigator key during normal route matching and reparsing; - making shell page keys identity-aware, so distinct routes remain distinct even when their `hashCode` values collide; - keying the nested `_CustomNavigator` wrapper with a value-equal `ValueKey` of its navigator key; - assigning scoped page and navigator keys only when an imperative push would otherwise collide with another live regular `ShellRoute` match; - deriving that scope from the persistent `ImperativeRouteMatch.pageKey`; - carrying existing shell matches through recursive imperative push matching, so nested key collisions are detected; - reparsing loaded inactive `StatefulShellRoute` branches and rebuilding their cached navigator widgets when route objects change; - scoping saved matches to their shell and resetting branches whose base location was removed; - identifying reparsed branches by their navigator key, including after nested `pushReplacement()`; - preserving branch and root observers when rebuilding inactive or preloaded navigators, honoring `notifyRootObserver`; - leaving the root navigator wrapper and normal configured-key navigation unchanged. ## Tests The regression test `push to a sibling shell route under the same parent shell route` forces both navigator-key and route-hash collisions. It covers: - pushing to a sibling shell route; - re-entering the first shell route while its original instance is still in the stack; - preserving navigator and page state across match-list reconstruction and `refresh()`; - detecting and scoping collisions in nested `ShellRouteMatch` trees; - popping back through both shell routes; - navigating with `go()` after the imperative stack is removed. Additional regressions cover the nested push sequence in [the review comment](#12549 (comment)), inactive loaded branches after a `RoutingConfig` update ([review comment](#12549 (comment))), and the exact flows from flutter/flutter#148712 and flutter/flutter#140586. Eight additional regression cases cover the follow-up review findings: a removed base route with a surviving push (with and without preload), nested `pushReplacement()`, a removed pushed route, and branch/root observer behavior for both preload and `notifyRootObserver` settings. The observer regression uses pageless `Navigator.push()` so a GoRouter rebuild cannot mask detached observers; it also verifies navigator-state preservation and updated `clipBehavior`. No screenshots are included because this is a routing/runtime fix. - Fixes flutter/flutter#148712 - Fixes flutter/flutter#140586 ## Validation - `flutter test --no-pub`: 471 passed - `flutter analyze --no-pub`: no issues - `flutter_plugin_tools validate --check-for-missing-changes --packages go_router`: no issues - `flutter_plugin_tools publish-check --packages go_router`: no issues - License check: passed Release note: This package uses batch releases, so the fix is recorded in `pending_changelogs/shell_route_key_collisions.yaml`; `pubspec.yaml` and `CHANGELOG.md` remain unchanged. ## Pre-Review Checklist [^1]: Regular contributors who have demonstrated familiarity with the repository guidelines only need to comment if the PR is not auto-exempted by repo tooling.
This PR fixes key collisions in
go_routerwhenpush()navigates from oneShellRoutebranch to a siblingShellRoutebranch under the same parent.In that scenario, both shell branches can temporarily coexist in the widget tree. Before this change, that could cause framework assertions such as
Multiple widgets used the same GlobalKeybecause:_CustomNavigatorwas keyed withGlobalObjectKey(navigatorKey.hashCode), which could collide when two navigator keys had the samehashCodeShellRouteMatchreused the configuration-timeShellRoute.navigatorKeyShellRouteMatch.pageKeywas derived only fromroute.hashCode, so multiple matches for the sameShellRoutecould share page identityThis PR fixes the problem for regular
ShellRouteby:GlobalKey<NavigatorState>for eachShellRouteMatchpageKeyfrom that per-match navigator key_CustomNavigatorwithGlobalObjectKey(navigatorKey)instead ofGlobalObjectKey(navigatorKey.hashCode)The change is intentionally scoped to
ShellRouteonly.StatefulShellRoutekeeps its existing stablepageKeybehavior because applying per-match page identity there can create duplicateStatefulNavigationShellinstances with the same internalGlobalKey.This PR also adds a regression test that reproduces the old crash by forcing a navigator key hash collision, verifies that pushing to a sibling shell route no longer crashes, and confirms that the previous shell route state is preserved after
pop().No screenshots are included since this is a routing/runtime fix rather than a visual UI change.
Pre-Review Checklist
[shared_preferences]///).