From b2446c5354b2b66a798cedaf4998d789ba46cb7d Mon Sep 17 00:00:00 2001 From: Danyal Ahmed <58849388+danyalahmed1995@users.noreply.github.com> Date: Tue, 16 Jun 2026 03:29:51 +0500 Subject: [PATCH 1/4] [go_router] Prevent inactive shell branches handling system back --- packages/go_router/lib/src/builder.dart | 28 +++- packages/go_router/lib/src/route.dart | 20 ++- ...stateful_shell_route_system_back_test.dart | 149 ++++++++++++++++++ 3 files changed, 195 insertions(+), 2 deletions(-) diff --git a/packages/go_router/lib/src/builder.dart b/packages/go_router/lib/src/builder.dart index 7594513e8264..83e59005e2a9 100644 --- a/packages/go_router/lib/src/builder.dart +++ b/packages/go_router/lib/src/builder.dart @@ -2,6 +2,7 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'package:flutter/foundation.dart'; import 'package:flutter/widgets.dart'; import 'configuration.dart'; @@ -135,6 +136,7 @@ class _CustomNavigator extends StatefulWidget { required this.errorBuilder, required this.errorPageBuilder, required this.requestFocus, + this.navigatorActive, }); final GlobalKey navigatorKey; @@ -153,6 +155,7 @@ class _CustomNavigator extends StatefulWidget { final GoRouterWidgetBuilder? errorBuilder; final GoRouterPageBuilder? errorPageBuilder; final bool requestFocus; + final ValueListenable? navigatorActive; @override State createState() => _CustomNavigatorState(); @@ -277,6 +280,7 @@ class _CustomNavigatorState extends State<_CustomNavigator> { RouteMatchList matchList, List? observers, String? restorationScopeId, + ValueListenable? navigatorActive, ) { return PopScope( // Prevent ShellRoute from being popped, for example @@ -294,6 +298,7 @@ class _CustomNavigatorState extends State<_CustomNavigator> { configuration: widget.configuration, observers: observers ?? const [], onPopPageWithRouteMatch: widget.onPopPageWithRouteMatch, + navigatorActive: navigatorActive, // This is used to recursively build pages under this shell route. errorBuilder: widget.errorBuilder, errorPageBuilder: widget.errorPageBuilder, @@ -368,12 +373,15 @@ class _CustomNavigatorState extends State<_CustomNavigator> { Page _buildPlatformAdapterPage(BuildContext context, GoRouterState state, Widget child) { // build the page based on app type _cacheAppType(context); + final Widget pageChild = widget.navigatorActive == null + ? child + : _BranchNavigatorPopScope(navigatorActive: widget.navigatorActive!, child: child); return _pageBuilderForAppType!( key: state.pageKey, name: state.name ?? state.path, arguments: {...state.pathParameters, ...state.uri.queryParameters}, restorationId: state.pageKey.value, - child: child, + child: pageChild, ); } @@ -441,3 +449,21 @@ class _CustomNavigatorState extends State<_CustomNavigator> { ); } } + +class _BranchNavigatorPopScope extends StatelessWidget { + const _BranchNavigatorPopScope({required this.navigatorActive, required this.child}); + + final ValueListenable navigatorActive; + final Widget child; + + @override + Widget build(BuildContext context) { + return ValueListenableBuilder( + valueListenable: navigatorActive, + child: child, + builder: (BuildContext context, bool isActive, Widget? child) { + return PopScope(canPop: isActive, child: child!); + }, + ); + } +} diff --git a/packages/go_router/lib/src/route.dart b/packages/go_router/lib/src/route.dart index 6d75560a93cd..7e91d5cddab2 100644 --- a/packages/go_router/lib/src/route.dart +++ b/packages/go_router/lib/src/route.dart @@ -55,6 +55,7 @@ typedef NavigatorBuilder = RouteMatchList matchList, List? observers, String? restorationScopeId, + ValueListenable? navigatorActive, ); /// Signature for function used in [RouteBase.onExit]. @@ -567,6 +568,7 @@ class ShellRouteContext { List? observers, bool notifyRootObserver, String? restorationScopeId, + ValueListenable? navigatorActive, ) { final effectiveObservers = [...?observers]; @@ -583,6 +585,7 @@ class ShellRouteContext { routeMatchList, effectiveObservers, restorationScopeId, + navigatorActive, ); } } @@ -731,6 +734,7 @@ class ShellRoute extends ShellRouteBase { observers, notifyRootObserver, restorationScopeId, + null, ); return builder!(context, state, navigator); } @@ -749,6 +753,7 @@ class ShellRoute extends ShellRouteBase { observers, notifyRootObserver, restorationScopeId, + null, ); return pageBuilder!(context, state, navigator); } @@ -1375,12 +1380,21 @@ class StatefulNavigationShellState extends State with R branch.observers, route.notifyRootObserver, branch.restorationScopeId, + branchState.navigatorActive, ); } + _updateActiveBranchNavigatorFlags(); _cleanUpObsoleteBranches(); } + void _updateActiveBranchNavigatorFlags() { + for (var i = 0; i < route.branches.length; i++) { + final _StatefulShellBranchState? branchState = _branchState[route.branches[i]]; + branchState?.navigatorActive.value = i == currentIndex; + } + } + void _preloadBranches() { for (var i = 0; i < route.branches.length; i++) { final StatefulShellBranch branch = route.branches[i]; @@ -1398,15 +1412,17 @@ class StatefulNavigationShellState extends State with R }); assert(match != null); + final _StatefulShellBranchState branchState = _branchStateFor(branch, false); + branchState.navigatorActive.value = false; final Widget navigator = widget.shellRouteContext.navigatorBuilder( branch.navigatorKey, match!, matchList, branch.observers, branch.restorationScopeId, + branchState.navigatorActive, ); - final _StatefulShellBranchState branchState = _branchStateFor(branch, false); branchState.location.value = matchList; branchState.navigator = navigator; } @@ -1491,9 +1507,11 @@ class _StatefulShellBranchState { Widget? navigator; final _RestorableRouteMatchList location; + final ValueNotifier navigatorActive = ValueNotifier(false); void dispose() { location.dispose(); + navigatorActive.dispose(); } } diff --git a/packages/go_router/test/stateful_shell_route_system_back_test.dart b/packages/go_router/test/stateful_shell_route_system_back_test.dart index 7e66d58827a1..3871965a8052 100644 --- a/packages/go_router/test/stateful_shell_route_system_back_test.dart +++ b/packages/go_router/test/stateful_shell_route_system_back_test.dart @@ -4,6 +4,7 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:go_router/go_router.dart'; @@ -87,6 +88,93 @@ void main() { await tester.pumpAndSettle(); expect(find.text('Home'), findsOneWidget); }); + + testWidgets('does not pop inactive StatefulShellRoute branches', (WidgetTester tester) async { + final List pops = []; + StatefulNavigationShell? navigationShell; + addTearDown(() async { + await tester.pumpWidget(const SizedBox.shrink()); + await tester.pump(); + }); + + final GoRouter router = await createRouter( + [ + StatefulShellRoute.indexedStack( + builder: (BuildContext context, GoRouterState state, StatefulNavigationShell shell) { + navigationShell = shell; + return shell; + }, + branches: [ + StatefulShellBranch( + observers: [_RecordingNavigatorObserver('/A', pops)], + routes: [ + GoRoute( + path: '/A1', + builder: (_, _) => const _BranchScreen(title: 'Stack A - 1', canPop: false), + routes: [ + GoRoute( + path: '/A2', + builder: (_, _) => const _BranchScreen(title: 'Stack A - 2'), + routes: [ + GoRoute( + path: '/A3', + builder: (_, _) => const _BranchScreen(title: 'Stack A - 3'), + ), + ], + ), + ], + ), + ], + ), + StatefulShellBranch( + observers: [_RecordingNavigatorObserver('/B', pops)], + routes: [ + GoRoute( + path: '/B1', + builder: (_, _) => const _BranchScreen(title: 'Stack B - 1', canPop: false), + ), + ], + ), + StatefulShellBranch( + observers: [_RecordingNavigatorObserver('/C', pops)], + routes: [ + GoRoute( + path: '/C1', + builder: (_, _) => const _BranchScreen(title: 'Stack C - 1', canPop: false), + routes: [ + GoRoute( + path: '/C2', + builder: (_, _) => const _BranchScreen(title: 'Stack C - 2'), + ), + ], + ), + ], + ), + ], + ), + ], + tester, + initialLocation: '/A1', + ); + + router.go('/A1/A2/A3'); + await tester.pumpAndSettle(); + expect(find.text('Stack A - 3'), findsOneWidget); + + router.go('/C1/C2'); + await tester.pumpAndSettle(); + expect(find.text('Stack C - 2'), findsOneWidget); + + navigationShell!.goBranch(1); + await tester.pumpAndSettle(); + expect(find.text('Stack B - 1'), findsOneWidget); + + await simulateAndroidPredictiveBackGesture(tester); + await tester.pump(); + + expect(find.text('Stack B - 1'), findsOneWidget); + expect(pops, isEmpty); + }); }); } @@ -172,3 +260,64 @@ class _TestAppState extends State<_TestApp> { return MaterialApp.router(routerConfig: _router); } } + +class _BranchScreen extends StatelessWidget { + const _BranchScreen({required this.title, this.canPop = true}); + + final String title; + final bool canPop; + + @override + Widget build(BuildContext context) { + return PopScope( + canPop: canPop, + child: Scaffold(body: Center(child: Text(title))), + ); + } +} + +class _RecordingNavigatorObserver extends NavigatorObserver { + _RecordingNavigatorObserver(this.branch, this.pops); + + final String branch; + final List pops; + + @override + void didPop(Route route, Route? previousRoute) { + pops.add('$branch ${route.settings.name} -> ${previousRoute?.settings.name}'); + } +} + +Future simulateAndroidPredictiveBackGesture(WidgetTester tester) async { + await _handleAndroidPredictiveBackMessage( + tester, + const MethodCall('startBackGesture', { + 'touchOffset': [5.0, 300.0], + 'progress': 0.0, + 'swipeEdge': 0, + }), + ); + await tester.pump(); + + await _handleAndroidPredictiveBackMessage( + tester, + const MethodCall('updateBackGestureProgress', { + 'x': 100.0, + 'y': 300.0, + 'progress': 0.35, + 'swipeEdge': 0, + }), + ); + await tester.pump(); + + await _handleAndroidPredictiveBackMessage(tester, const MethodCall('commitBackGesture')); +} + +Future _handleAndroidPredictiveBackMessage(WidgetTester tester, MethodCall methodCall) async { + final ByteData message = const StandardMethodCodec().encodeMethodCall(methodCall); + await tester.binding.defaultBinaryMessenger.handlePlatformMessage( + 'flutter/backgesture', + message, + (ByteData? _) {}, + ); +} From 62c1d0326d32e2fbdae8d95a7365ab3a511023ee Mon Sep 17 00:00:00 2001 From: Danyal Ahmed <58849388+danyalahmed1995@users.noreply.github.com> Date: Tue, 16 Jun 2026 03:36:15 +0500 Subject: [PATCH 2/4] [go_router] Prevent inactive shell branches handling system back --- .../go_router/test/stateful_shell_route_system_back_test.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/go_router/test/stateful_shell_route_system_back_test.dart b/packages/go_router/test/stateful_shell_route_system_back_test.dart index 3871965a8052..8389fe5fda4e 100644 --- a/packages/go_router/test/stateful_shell_route_system_back_test.dart +++ b/packages/go_router/test/stateful_shell_route_system_back_test.dart @@ -90,7 +90,7 @@ void main() { }); testWidgets('does not pop inactive StatefulShellRoute branches', (WidgetTester tester) async { - final List pops = []; + final pops = []; StatefulNavigationShell? navigationShell; addTearDown(() async { await tester.pumpWidget(const SizedBox.shrink()); From 3eec9e6ed3099786f340084ff32a5e26b8e445c7 Mon Sep 17 00:00:00 2001 From: Danyal Ahmed <58849388+danyalahmed1995@users.noreply.github.com> Date: Tue, 16 Jun 2026 23:05:17 +0500 Subject: [PATCH 3/4] [go_router] Add pending changelog for predictive back fix --- .../pending_changelogs/change_2026_06_16_1781632636645.yaml | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 packages/go_router/pending_changelogs/change_2026_06_16_1781632636645.yaml diff --git a/packages/go_router/pending_changelogs/change_2026_06_16_1781632636645.yaml b/packages/go_router/pending_changelogs/change_2026_06_16_1781632636645.yaml new file mode 100644 index 000000000000..d96867104747 --- /dev/null +++ b/packages/go_router/pending_changelogs/change_2026_06_16_1781632636645.yaml @@ -0,0 +1,3 @@ +changelog: | + - Fixes Android system/predictive back popping inactive StatefulShellBranch navigators. +version: patch From 23006898ea8db90f6c404224dce13c74406c7fd7 Mon Sep 17 00:00:00 2001 From: Danyal Ahmed <58849388+danyalahmed1995@users.noreply.github.com> Date: Tue, 16 Jun 2026 23:28:04 +0500 Subject: [PATCH 4/4] [go_router] Make navigatorActive builder parameter optional --- packages/go_router/lib/src/builder.dart | 4 ++-- packages/go_router/lib/src/route.dart | 16 +++++++--------- 2 files changed, 9 insertions(+), 11 deletions(-) diff --git a/packages/go_router/lib/src/builder.dart b/packages/go_router/lib/src/builder.dart index 83e59005e2a9..13a4f06ba6c6 100644 --- a/packages/go_router/lib/src/builder.dart +++ b/packages/go_router/lib/src/builder.dart @@ -279,9 +279,9 @@ class _CustomNavigatorState extends State<_CustomNavigator> { ShellRouteMatch match, RouteMatchList matchList, List? observers, - String? restorationScopeId, + String? restorationScopeId, { ValueListenable? navigatorActive, - ) { + }) { return PopScope( // Prevent ShellRoute from being popped, for example // by an iOS back gesture, when the route has active sub-routes. diff --git a/packages/go_router/lib/src/route.dart b/packages/go_router/lib/src/route.dart index 7e91d5cddab2..ae79bbdd603b 100644 --- a/packages/go_router/lib/src/route.dart +++ b/packages/go_router/lib/src/route.dart @@ -54,9 +54,9 @@ typedef NavigatorBuilder = ShellRouteMatch match, RouteMatchList matchList, List? observers, - String? restorationScopeId, + String? restorationScopeId, { ValueListenable? navigatorActive, - ); + }); /// Signature for function used in [RouteBase.onExit]. /// @@ -567,9 +567,9 @@ class ShellRouteContext { BuildContext context, List? observers, bool notifyRootObserver, - String? restorationScopeId, + String? restorationScopeId, { ValueListenable? navigatorActive, - ) { + }) { final effectiveObservers = [...?observers]; if (notifyRootObserver) { @@ -585,7 +585,7 @@ class ShellRouteContext { routeMatchList, effectiveObservers, restorationScopeId, - navigatorActive, + navigatorActive: navigatorActive, ); } } @@ -734,7 +734,6 @@ class ShellRoute extends ShellRouteBase { observers, notifyRootObserver, restorationScopeId, - null, ); return builder!(context, state, navigator); } @@ -753,7 +752,6 @@ class ShellRoute extends ShellRouteBase { observers, notifyRootObserver, restorationScopeId, - null, ); return pageBuilder!(context, state, navigator); } @@ -1380,7 +1378,7 @@ class StatefulNavigationShellState extends State with R branch.observers, route.notifyRootObserver, branch.restorationScopeId, - branchState.navigatorActive, + navigatorActive: branchState.navigatorActive, ); } _updateActiveBranchNavigatorFlags(); @@ -1420,7 +1418,7 @@ class StatefulNavigationShellState extends State with R matchList, branch.observers, branch.restorationScopeId, - branchState.navigatorActive, + navigatorActive: branchState.navigatorActive, ); branchState.location.value = matchList;