Skip to content

[go_router] Fix key collisions when sibling ShellRoutes coexist during push - #11143

Closed
Gorniv wants to merge 2 commits into
flutter:mainfrom
Gorniv:main
Closed

Gorniv wants to merge 2 commits into
flutter:mainfrom
Gorniv:main

Conversation

@Gorniv

@Gorniv Gorniv commented Feb 28, 2026

Copy link
Copy Markdown

This PR fixes key collisions in go_router when push() navigates from one ShellRoute branch to a sibling ShellRoute branch 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 GlobalKey because:

  • _CustomNavigator was keyed with GlobalObjectKey(navigatorKey.hashCode), which could collide when two navigator keys had the same hashCode
  • ShellRouteMatch reused the configuration-time ShellRoute.navigatorKey
  • ShellRouteMatch.pageKey was derived only from route.hashCode, so multiple matches for the same ShellRoute could share page identity

This PR fixes the problem for regular ShellRoute by:

  • creating a fresh GlobalKey<NavigatorState> for each ShellRouteMatch
  • deriving a unique pageKey from that per-match navigator key
  • keying _CustomNavigator with GlobalObjectKey(navigatorKey) instead of GlobalObjectKey(navigatorKey.hashCode)

The change is intentionally scoped to ShellRoute only. StatefulShellRoute keeps its existing stable pageKey behavior because applying per-match page identity there can create duplicate StatefulNavigationShell instances with the same internal GlobalKey.

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

  • I read the [Contributor Guide] and followed the process outlined there for submitting PRs.
  • I read the [Tree Hygiene] page, which explains my responsibilities.
  • I read and followed the [relevant style guides] and ran [the auto-formatter].
  • I signed the [CLA].
  • The title of the PR starts with the name of the package surrounded by square brackets, e.g. [shared_preferences]
  • I [linked to at least one issue that this PR fixes] in the description above.
  • I followed [the version and CHANGELOG instructions], using [semantic versioning] and the [repository CHANGELOG style], or I have commented below to indicate which documented exception this PR falls under[^1].
  • I updated/added any relevant documentation (doc comments with ///).
  • I added new tests to check the change I am making, or I have commented below to indicate which [test exemption] this PR falls under[^1].
  • All existing and new tests are passing.

@Gorniv
Gorniv requested a review from chunhtai as a code owner February 28, 2026 22:08
@github-actions github-actions Bot added p: go_router triage-framework Should be looked at in framework triage labels Feb 28, 2026

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This doesn't feel right. maybe we want a different way to create the persistent globalkey?

@stuartmorgan-g

Copy link
Copy Markdown
Contributor

From triage: @Gorniv Are you planning on updating this PR based on the feedback above?

@justinmc

justinmc commented May 5, 2026

Copy link
Copy Markdown
Contributor

@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.

@justinmc justinmc closed this May 5, 2026
auto-submit Bot pushed a commit that referenced this pull request Oct 14, 2026
…#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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

p: go_router triage-framework Should be looked at in framework triage

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants