Skip to content

Commit 293106d

Browse files
committed
Fix StatefulShellRoute PopScope behavior to respect back button (#181945)
1 parent 5299279 commit 293106d

4 files changed

Lines changed: 198 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: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1569,7 +1569,15 @@ class StatefulNavigationShellState extends State<StatefulNavigationShell>
15691569
)
15701570
.toList();
15711571

1572-
return widget.containerBuilder(context, widget, children);
1572+
return PopScope(
1573+
canPop: widget.currentIndex == 0,
1574+
onPopInvokedWithResult: (bool didPop, Object? result) {
1575+
if (!didPop && widget.currentIndex != 0) {
1576+
goBranch(0);
1577+
}
1578+
},
1579+
child: widget.containerBuilder(context, widget, children),
1580+
);
15731581
}
15741582
}
15751583

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: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
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+
116+
testWidgets(
117+
'StatefulShellRoute switches to root branch by default on back button',
118+
(WidgetTester tester) async {
119+
final routes = <RouteBase>[
120+
StatefulShellRoute.indexedStack(
121+
builder:
122+
(
123+
BuildContext context,
124+
GoRouterState state,
125+
StatefulNavigationShell navigationShell,
126+
) {
127+
return Scaffold(
128+
body: navigationShell,
129+
bottomNavigationBar: BottomNavigationBar(
130+
currentIndex: navigationShell.currentIndex,
131+
onTap: (int index) => navigationShell.goBranch(index),
132+
items: const <BottomNavigationBarItem>[
133+
BottomNavigationBarItem(
134+
icon: Icon(Icons.home),
135+
label: 'A',
136+
),
137+
BottomNavigationBarItem(
138+
icon: Icon(Icons.business),
139+
label: 'B',
140+
),
141+
],
142+
),
143+
);
144+
},
145+
branches: <StatefulShellBranch>[
146+
StatefulShellBranch(
147+
routes: <RouteBase>[
148+
GoRoute(
149+
path: '/tabA',
150+
builder: (BuildContext context, GoRouterState state) =>
151+
const DummyScreen(key: ValueKey<String>('tabA')),
152+
),
153+
],
154+
),
155+
StatefulShellBranch(
156+
routes: <RouteBase>[
157+
GoRoute(
158+
path: '/tabB',
159+
builder: (BuildContext context, GoRouterState state) =>
160+
const DummyScreen(key: ValueKey<String>('tabB')),
161+
),
162+
],
163+
),
164+
],
165+
),
166+
];
167+
168+
final GoRouter router = await createRouter(
169+
routes,
170+
tester,
171+
initialLocation: '/tabA',
172+
);
173+
174+
expect(find.byKey(const ValueKey<String>('tabA')), findsOneWidget);
175+
176+
router.go('/tabB');
177+
await tester.pumpAndSettle();
178+
expect(find.byKey(const ValueKey<String>('tabB')), findsOneWidget);
179+
180+
await simulateAndroidBackButton(tester);
181+
await tester.pumpAndSettle();
182+
183+
expect(find.byKey(const ValueKey<String>('tabA')), findsOneWidget);
184+
},
185+
);
186+
}

0 commit comments

Comments
 (0)