Skip to content

Commit 0135340

Browse files
committed
Fix StatefulShellRoute PopScope behavior to respect back button (#181945)
1 parent f3a5acb commit 0135340

4 files changed

Lines changed: 128 additions & 2 deletions

File tree

packages/go_router/lib/src/delegate.dart

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,6 @@ class GoRouterDelegate extends RouterDelegate<RouteMatchList>
124124
while (walker is ShellRouteMatch) {
125125
final NavigatorState potentialCandidate =
126126
walker.navigatorKey.currentState!;
127-
128127
final ModalRoute<dynamic>? modalRoute = ModalRoute.of(
129128
potentialCandidate.context,
130129
);

packages/go_router/lib/src/route.dart

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import 'match.dart';
1616
import 'path_utils.dart';
1717
import 'router.dart';
1818
import 'state.dart';
19+
import 'delegate.dart';
1920

2021
/// The page builder for [GoRoute].
2122
typedef GoRouterPageBuilder =
@@ -1541,7 +1542,15 @@ class StatefulNavigationShellState extends State<StatefulNavigationShell>
15411542
)
15421543
.toList();
15431544

1544-
return widget.containerBuilder(context, widget, children);
1545+
return PopScope(
1546+
canPop: widget.currentIndex == 0,
1547+
onPopInvokedWithResult: (bool didPop, Object? result) {
1548+
if (!didPop && widget.currentIndex != 0) {
1549+
goBranch(0);
1550+
}
1551+
},
1552+
child: widget.containerBuilder(context, widget, children),
1553+
);
15451554
}
15461555
}
15471556

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
changelog: |
2+
- Fix StatefulShellRoute PopScope behavior to orchestrate back button pops to the root branch when inner navigators cannot pop.
3+
version: patch
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
// Copyright 2013 The Flutter Authors
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
import 'package:flutter/material.dart';
6+
import 'package:flutter_test/flutter_test.dart';
7+
import 'package:go_router/go_router.dart';
8+
9+
import 'test_helpers.dart';
10+
11+
void main() {
12+
testWidgets(
13+
'PopScope in StatefulShellRoute branch works on subsequent visits',
14+
(WidgetTester tester) async {
15+
int tabBPopCount = 0;
16+
17+
final routes = <RouteBase>[
18+
StatefulShellRoute.indexedStack(
19+
builder:
20+
(
21+
BuildContext context,
22+
GoRouterState state,
23+
StatefulNavigationShell navigationShell,
24+
) {
25+
return Scaffold(
26+
body: navigationShell,
27+
bottomNavigationBar: BottomNavigationBar(
28+
currentIndex: navigationShell.currentIndex,
29+
onTap: (int index) => navigationShell.goBranch(index),
30+
items: const <BottomNavigationBarItem>[
31+
BottomNavigationBarItem(
32+
icon: Icon(Icons.home),
33+
label: 'A',
34+
),
35+
BottomNavigationBarItem(
36+
icon: Icon(Icons.business),
37+
label: 'B',
38+
),
39+
],
40+
),
41+
);
42+
},
43+
branches: <StatefulShellBranch>[
44+
StatefulShellBranch(
45+
routes: <RouteBase>[
46+
GoRoute(
47+
path: '/tabA',
48+
builder: (BuildContext context, GoRouterState state) =>
49+
const DummyScreen(key: ValueKey<String>('tabA')),
50+
),
51+
],
52+
),
53+
StatefulShellBranch(
54+
routes: <RouteBase>[
55+
GoRoute(
56+
path: '/tabB',
57+
builder: (BuildContext context, GoRouterState state) {
58+
return PopScope(
59+
canPop: false,
60+
onPopInvokedWithResult: (bool didPop, Object? result) {
61+
tabBPopCount++;
62+
if (!didPop) {
63+
context.go('/tabA');
64+
}
65+
},
66+
child: const DummyScreen(key: ValueKey<String>('tabB')),
67+
);
68+
},
69+
),
70+
],
71+
),
72+
],
73+
),
74+
];
75+
76+
final GoRouter router = await createRouter(
77+
routes,
78+
tester,
79+
initialLocation: '/tabA',
80+
);
81+
82+
// 1. Visit Tab A
83+
expect(find.byKey(const ValueKey<String>('tabA')), findsOneWidget);
84+
expect(find.byKey(const ValueKey<String>('tabB')), findsNothing);
85+
86+
// 2. Switch to Tab B
87+
router.go('/tabB');
88+
await tester.pumpAndSettle();
89+
expect(find.byKey(const ValueKey<String>('tabB')), findsOneWidget);
90+
91+
// 3. Press back button on Tab B (First Visit)
92+
await simulateAndroidBackButton(tester);
93+
await tester.pumpAndSettle();
94+
95+
// Should have switched back to Tab A
96+
expect(find.byKey(const ValueKey<String>('tabA')), findsOneWidget);
97+
expect(tabBPopCount, 1);
98+
99+
// 4. Switch to Tab B again (Second Visit)
100+
router.go('/tabB');
101+
await tester.pumpAndSettle();
102+
expect(find.byKey(const ValueKey<String>('tabB')), findsOneWidget);
103+
104+
// 5. Press back button on Tab B (Second Visit)
105+
await simulateAndroidBackButton(tester);
106+
await tester.pumpAndSettle();
107+
108+
// Verify that the PopScope was triggered again
109+
expect(tabBPopCount, 2);
110+
111+
// Should have switched back to Tab A again
112+
expect(find.byKey(const ValueKey<String>('tabA')), findsOneWidget);
113+
},
114+
);
115+
}

0 commit comments

Comments
 (0)