Skip to content

[go_router] Stop inactive StatefulShellRoute branches from vetoing pops - #12972

Merged
auto-submit[bot] merged 9 commits into
flutter:mainfrom
IbrarIlyas:fix/193098-inactive-branch-popscope
Nov 4, 2026
Merged

auto-submit[bot] merged 9 commits into
flutter:mainfrom
IbrarIlyas:fix/193098-inactive-branch-popscope

Conversation

@IbrarIlyas

@IbrarIlyas IbrarIlyas commented Sep 22, 2026 •

Copy link
Copy Markdown
Contributor

The Navigator of every StatefulShellRoute branch is wrapped in a PopScope that
keeps the shell itself from being popped, for example by an iOS back gesture,
while the branch has an active sub-route.

Inactive branches stay mounted, so their PopScope stays registered as a pop
entry of the same enclosing route as the active one, and ModalRoute only allows
a pop if every registered entry allows it. Switching away from a branch that
still had a sub-route therefore suppressed the system back button for the whole
app: popRoute() reported the pop as handled without popping anything, so Android
never closed the activity.

The guard now only applies to the active branch. StatefulNavigationShell
publishes which of its branches is active through an ActiveBranchScope inherited
widget, and the PopScope depends on it, so canPop is also kept up to date when
the user switches branches without the route matches of a branch changing.
Behavior is unchanged for a plain ShellRoute, which has no enclosing
ActiveBranchScope.

Tests added to test/stateful_shell_route_system_back_test.dart, next to the
existing issue 120353 gesture tests: the issue's repro, back still popping the
sub-route once the branch is active again, and a per-branch canPop check
confirming the active branch still guards the shell. Also verified manually on
an Android 15 device.

Fixes flutter/flutter#193098

Replace this paragraph with a description of what this PR is changing or adding, and why. Consider including before/after screenshots.

List which issues are fixed by this PR. You must list at least one issue.

Pre-Review Checklist

If you need help, consider asking for advice on the #hackers-new channel on Discord.

Note: The Flutter team is currently trialing the use of Gemini Code Assist for GitHub. Comments from the gemini-code-assist bot should not be taken as authoritative feedback from the Flutter team. If you find its comments useful you can update your code accordingly, but if you are unsure or disagree with the feedback, please feel free to wait for a Flutter team member's review for guidance on which automated comments should be addressed.

Footnotes

  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. ↩ ↩2

@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 introduces ActiveBranchScope, an InheritedWidget that tracks whether a subtree belongs to the currently active branch of a StatefulShellRoute. It updates _CustomNavigatorState and _BranchNavigatorProxy to utilize this scope, ensuring that only the active branch's PopScope can veto pops, which prevents inactive branches with active sub-routes from blocking system back navigation. Additionally, regression tests and a changelog entry are included. There are no review comments, so no further feedback is provided.

@elliette
elliette marked this pull request as draft September 22, 2026 20:31
@elliette

Copy link
Copy Markdown
Member

Thanks for the contribution! You haven’t completed all of the items in the PR checklist above, so I’m assuming this is a work in progress and am marking it as a Draft. Please review the checklist, updating the PR as appropriate, and when that’s complete please feel free to mark the PR as ready for review.

The Navigator of every StatefulShellRoute branch is wrapped in a PopScope
that keeps the shell itself from being popped, for example by an iOS back
gesture, while the branch has an active sub-route.

Inactive branches stay mounted, so their PopScope stays registered as a pop
entry of the same enclosing route as the active one, and ModalRoute only
allows a pop if every registered entry allows it. Switching away from a
branch that still had a sub-route therefore suppressed the system back
button for the whole app: popRoute() reported the pop as handled without
popping anything, so Android never closed the activity.

The guard now only applies to the active branch. StatefulNavigationShell
publishes which of its branches is active through an ActiveBranchScope
inherited widget, and the PopScope depends on it, so canPop is also kept up
to date when the user switches branches without the route matches of a
branch changing. Behavior is unchanged for a plain ShellRoute, which has no
enclosing ActiveBranchScope.

Fixes flutter/flutter#193098
@IbrarIlyas
IbrarIlyas force-pushed the fix/193098-inactive-branch-popscope branch from 8693ac6 to 01aa7fc Compare September 23, 2026 06:47
@IbrarIlyas
IbrarIlyas marked this pull request as ready for review September 23, 2026 06:48

@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 introduces ActiveBranchScope, an InheritedWidget that tracks whether a subtree belongs to the active branch of a StatefulShellRoute. This allows the PopScope of shell route navigators to only veto pops when their branch is active, resolving an issue where the system back button was ignored after switching branches. Regression tests and a changelog entry are also added. The review feedback recommends removing the generic type argument from the PopScope cast in the test file to prevent compilation errors on older Flutter versions.

@IbrarIlyas
IbrarIlyas force-pushed the fix/193098-inactive-branch-popscope branch from 7e79adf to f01624e Compare September 23, 2026 13:43
@IbrarIlyas

IbrarIlyas commented Sep 28, 2026 •

Copy link
Copy Markdown
Contributor Author

@elliette @ianloic @sethladd Checklist is done now, so this is ready for review. Its my first time 🙂

@Piinks
Piinks self-requested a review September 29, 2026 20:58
@IbrarIlyas

Copy link
Copy Markdown
Contributor Author

Hi Flutter team,

I’m following up on this PR. I have addressed the review feedback, updated the test accordingly, and synced the branch with main.

The PR is currently waiting for the CICD label and tests to run. I would appreciate it if you could review the latest implementation whenever convenient.

Thank you for your time.

@IbrarIlyas

IbrarIlyas commented Oct 5, 2026 •

Copy link
Copy Markdown
Contributor Author

can you guide me @Piinks what is the issue in this PR so that i can make sure to fix it.

@Piinks Piinks added the CICD Run CI/CD label Oct 12, 2026
@flutter-dashboard

Copy link
Copy Markdown

This pull request is not mergeable in its current state, likely because of a merge conflict. Pre-submit CI jobs were not triggered. Pushing a new commit to this branch that resolves the issue will result in pre-submit jobs being scheduled.

@Piinks

Piinks commented Oct 12, 2026

Copy link
Copy Markdown
Contributor

Hi @IbrarIlyas, please refrain from tagging maintainers directly. This will be reviewed shortly.

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

Can you also add this test in order to ensure coverage for preload: true?

Details
testWidgets(
  'Preloaded branch (preload: true) with deep initialLocation does not veto pop while inactive, and guards shell once activated',
  (WidgetTester tester) async {
    final router = GoRouter(
      initialLocation: '/home',
      routes: <RouteBase>[
        StatefulShellRoute.indexedStack(
          builder:
              (
                BuildContext context,
                GoRouterState state,
                StatefulNavigationShell shell,
              ) => Scaffold(
                body: shell,
                bottomNavigationBar: Row(
                  children: <Widget>[
                    FilledButton(
                      key: const Key('toHome'),
                      onPressed: () => shell.goBranch(0),
                      child: const Text('Home'),
                    ),
                    FilledButton(
                      key: const Key('toSettings'),
                      onPressed: () => shell.goBranch(1),
                      child: const Text('Settings'),
                    ),
                  ],
                ),
              ),
          branches: <StatefulShellBranch>[
            StatefulShellBranch(
              routes: <RouteBase>[
                GoRoute(
                  path: '/home',
                  builder: (BuildContext context, GoRouterState state) =>
                      const Scaffold(body: Text('Home Screen')),
                ),
              ],
            ),
            StatefulShellBranch(
              preload: true,
              initialLocation: '/settings/general',
              routes: <RouteBase>[
                GoRoute(
                  path: '/settings',
                  builder: (BuildContext context, GoRouterState state) =>
                      const Scaffold(body: Text('Settings Screen')),
                  routes: <RouteBase>[
                    GoRoute(
                      path: 'general',
                      builder: (BuildContext context, GoRouterState state) =>
                          const Scaffold(body: Text('General Screen')),
                    ),
                  ],
                ),
              ],
            ),
          ],
        ),
      ],
    );
    addTearDown(router.dispose);

    await tester.pumpWidget(MaterialApp.router(routerConfig: router));
    await tester.pumpAndSettle();

    expect(_branchCanPopValues(tester), <bool>[true, true]);
    expect(await tester.binding.handlePopRoute(), isFalse);

    await tester.tap(find.byKey(const Key('toSettings')));
    await tester.pumpAndSettle();
    expect(find.text('General Screen'), findsOneWidget);
    expect(_branchCanPopValues(tester), <bool>[true, false]);

    expect(await tester.binding.handlePopRoute(), isTrue);
    await tester.pumpAndSettle();
    expect(find.text('Settings Screen'), findsOneWidget);
    expect(_branchCanPopValues(tester), <bool>[true, true]);
  },
);

- Annotate ActiveBranchScope with @internal.
- Use a typed widgetList in the system back test helper.
- Add a test covering a preloaded branch with a deep initialLocation.
@flutter-dashboard flutter-dashboard Bot removed the CICD Run CI/CD label Oct 13, 2026
@IbrarIlyas
IbrarIlyas force-pushed the fix/193098-inactive-branch-popscope branch from c2011df to cf4aaef Compare October 13, 2026 18:19
@IbrarIlyas

Copy link
Copy Markdown
Contributor Author

Thanks for the review! I've updated the PR:

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

LGTM thank you!

@Piinks

Piinks commented Oct 14, 2026

Copy link
Copy Markdown
Contributor

Can you address the merge conflicts?

@Piinks
Piinks requested a review from elliette October 20, 2026 20:58
@elliette elliette added the waiting for response The Flutter team cannot make further progress on this PR until the author responds label Oct 22, 2026
…-branch-popscope

# Conflicts:
#	packages/go_router/lib/src/builder.dart
#	packages/go_router/lib/src/route.dart
@github-actions github-actions Bot removed the waiting for response The Flutter team cannot make further progress on this PR until the author responds label Oct 26, 2026
@IbrarIlyas

Copy link
Copy Markdown
Contributor Author

Resolved the merge conflicts with latest main. Analyze and the go_router tests pass locally.

@elliette elliette added the CICD Run CI/CD label Nov 4, 2026

@elliette elliette left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Thank you, LGTM!

@elliette

elliette commented Nov 4, 2026 •

Copy link
Copy Markdown
Member

(Note: Updated the branch to get #13544, which should fix the failures).

@flutter-dashboard flutter-dashboard Bot removed the CICD Run CI/CD label Nov 4, 2026
@elliette elliette added CICD Run CI/CD autosubmit Merge PR when tree becomes green via auto submit App labels Nov 4, 2026
@auto-submit
auto-submit Bot merged commit 3c7764b into flutter:main Nov 4, 2026
13 checks passed
bkonyi pushed a commit to bkonyi/flutter that referenced this pull request Nov 5, 2026
…er#196047)

flutter/packages@670b30e...0478162

2026-11-05 mkmt@google.com [material_ui] Document RefreshIndicator mouse
behavior on desktop and web (flutter/packages#13286)
2026-11-05 45147003+samar70@users.noreply.github.com [material_ui] Make
SearchAnchor.bar respect SearchBarThemeData padding
(flutter/packages#13360)
2026-11-04 kevmoo@users.noreply.github.com [pointer_interceptor] Use
dart.library.js_interop in example export (flutter/packages#13558)
2026-11-04 louisehsu@google.com [in_app_purchase_storekit] Use
purchase(confirmIn:) for StoreKit 2 purchases (flutter/packages#13410)
2026-11-04 41366638+kirya355@users.noreply.github.com [material_ui] Fix
dismissal near minimum extent when shouldCloseOnMinExtent
(flutter/packages#12545)
2026-11-04 36861262+QuncCccccc@users.noreply.github.com [material_ui]
Add Material 3 Expressive `ElevatedButton` (flutter/packages#13084)
2026-11-04 108947675+IbrarIlyas@users.noreply.github.com [go_router]
Stop inactive StatefulShellRoute branches from vetoing pops
(flutter/packages#12972)
2026-11-04 engine-flutter-autoroll@skia.org Manual roll Flutter (stable)
from abaf9c5 to c197ca5 (14 revisions)
(flutter/packages#13542)
2026-11-04 stuart.morgan@gmail.com [in_app_purchase] Convert example app
to Swift (flutter/packages#13556)
2026-11-04 fluttergithubbot@gmail.com Sync release-go_router-18.1.3 to
main (flutter/packages#13559)
2026-11-04 fluttergithubbot@gmail.com Sync release-material_ui-1.10.0 to
main (flutter/packages#13546)
2026-11-04 43054281+camsim99@users.noreply.github.com
[camera_android_camerax] Add magic number guideline to `TESTING.md`
(flutter/packages#13081)
2026-11-04 fluttergithubbot@gmail.com Sync release-cupertino_ui-1.1.6 to
main (flutter/packages#13547)
2026-11-04 stuartmorgan@google.com [file_selector] Convert unit tests to
Kotlin (flutter/packages#13102)
2026-11-04 engine-flutter-autoroll@skia.org Roll Flutter from
9347202 to 3623d2d (25 revisions) (flutter/packages#13555)

If this roll has caused a breakage, revert this CL and set the roller
to dry run mode using the controls here:
https://autoroll.skia.org/r/flutter-packages-flutter-autoroll
Please CC flutter-ecosystem@google.com on the revert to ensure that a
human
is aware of the problem.

To file a bug in Flutter:
https://github.com/flutter/flutter/issues/new/choose

To report a problem with the AutoRoller itself, please file a bug:
https://issues.skia.org/issues/new?component=1389291&template=1850622

Documentation for the AutoRoller is here:
https://skia.googlesource.com/buildbot/+doc/main/autoroll/README.md
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

autosubmit Merge PR when tree becomes green via auto submit App CICD Run CI/CD p: go_router

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[go_router] Back button stops working app-wide after leaving a StatefulShellRoute branch that has sub-routes

4 participants