Skip to content

Commit 41c39ba

Browse files
authored
[go_router] Fix: Consistent PopScope Handling on Root Routes issue #140869 (#8045)
**Description:** This PR addresses issue [#140869](flutter/flutter#140869) related to back button handling on root routes in `GoRouterDelegate`. The current `_findCurrentNavigator()` function only assigns `NavigatorState` when it can pop, which prevents `PopScope` and custom back button logic from triggering on root routes, especially on Android. Additionally, the `onExit` callbacks on routes requiring custom exit handling are affected. **What This PR Changes:** 1. **Modification of `_findCurrentNavigator()`**: - Removed the conditional `canPop` check, setting `state = navigatorKey.currentState;` directly to ensure `NavigatorState` is always non-null, allowing consistent `PopScope` handling on root routes. 2. **Adjustment of `popRoute()` to Preserve `onExit` Logic**: - Updated `popRoute()` to call `maybePop()` unconditionally to make sure `PopScope` can handle back button actions on root pages. - Added fallback to `onExit` logic in `popRoute()` if `maybePop()` does not handle the pop, this way, any route-specific exit callbacks are still functional. **Impact**: - **Resolves Inconsistent Back Button Handling**: The fix enables `PopScope` and back button handling on root pages without affecting nested routes or shell routes. - **Maintains `onExit` Callback Functionality**: `onExit` is invoked when `maybePop()` doesn�t handle the pop, preserving custom exit behaviors. ---
1 parent c4fc0e0 commit 41c39ba

File tree

4 files changed

+75
-22
lines changed

4 files changed

+75
-22
lines changed

packages/go_router/CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
## 14.6.1
2+
3+
- Fixed `PopScope`, and `WillPopScop` was not handled properly in the Root routes.
4+
15
## 14.6.0
26

37
- Allows going to a path relatively by prefixing `./`
@@ -6,6 +10,7 @@
610

711
- Adds preload support to StatefulShellRoute, configurable via `preload` parameter on StatefulShellBranch.
812

13+
914
## 14.4.1
1015

1116
- Adds `missing_code_block_language_in_doc_comment` lint.

packages/go_router/lib/src/delegate.dart

Lines changed: 31 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -57,9 +57,13 @@ class GoRouterDelegate extends RouterDelegate<RouteMatchList>
5757
Future<bool> popRoute() async {
5858
final NavigatorState? state = _findCurrentNavigator();
5959
if (state != null) {
60-
return state.maybePop();
60+
final bool didPop = await state.maybePop(); // Call maybePop() directly
61+
if (didPop) {
62+
return true; // Return true if maybePop handled the pop
63+
}
6164
}
62-
// This should be the only place where the last GoRoute exit the screen.
65+
66+
// Fallback to onExit if maybePop did not handle the pop
6367
final GoRoute lastRoute = currentConfiguration.last.route;
6468
if (lastRoute.onExit != null && navigatorKey.currentContext != null) {
6569
return !(await lastRoute.onExit!(
@@ -68,6 +72,7 @@ class GoRouterDelegate extends RouterDelegate<RouteMatchList>
6872
.buildState(_configuration, currentConfiguration),
6973
));
7074
}
75+
7176
return false;
7277
}
7378

@@ -89,26 +94,29 @@ class GoRouterDelegate extends RouterDelegate<RouteMatchList>
8994
/// Pops the top-most route.
9095
void pop<T extends Object?>([T? result]) {
9196
final NavigatorState? state = _findCurrentNavigator();
92-
if (state == null) {
97+
if (state == null || !state.canPop()) {
9398
throw GoError('There is nothing to pop');
9499
}
95100
state.pop(result);
96101
}
97102

98103
NavigatorState? _findCurrentNavigator() {
99104
NavigatorState? state;
100-
if (navigatorKey.currentState?.canPop() ?? false) {
101-
state = navigatorKey.currentState;
102-
}
105+
state =
106+
navigatorKey.currentState; // Set state directly without canPop check
107+
103108
RouteMatchBase walker = currentConfiguration.matches.last;
104109
while (walker is ShellRouteMatch) {
105110
final NavigatorState potentialCandidate =
106111
walker.navigatorKey.currentState!;
107-
if (!ModalRoute.of(potentialCandidate.context)!.isCurrent) {
108-
// There is a pageless route on top of the shell route. it needs to be
109-
// popped first.
112+
113+
final ModalRoute<dynamic>? modalRoute =
114+
ModalRoute.of(potentialCandidate.context);
115+
if (modalRoute == null || !modalRoute.isCurrent) {
116+
// Stop if there is a pageless route on top of the shell route.
110117
break;
111118
}
119+
112120
if (potentialCandidate.canPop()) {
113121
state = walker.navigatorKey.currentState;
114122
}
@@ -117,14 +125,6 @@ class GoRouterDelegate extends RouterDelegate<RouteMatchList>
117125
return state;
118126
}
119127

120-
void _debugAssertMatchListNotEmpty() {
121-
assert(
122-
currentConfiguration.isNotEmpty,
123-
'You have popped the last page off of the stack,'
124-
' there are no pages left to show',
125-
);
126-
}
127-
128128
bool _handlePopPageWithRouteMatch(
129129
Route<Object?> route, Object? result, RouteMatchBase match) {
130130
if (route.willHandlePopInternally) {
@@ -154,6 +154,14 @@ class GoRouterDelegate extends RouterDelegate<RouteMatchList>
154154
return false;
155155
}
156156

157+
void _debugAssertMatchListNotEmpty() {
158+
assert(
159+
currentConfiguration.isNotEmpty,
160+
'You have popped the last page off of the stack,'
161+
' there are no pages left to show',
162+
);
163+
}
164+
157165
void _completeRouteMatch(Object? result, RouteMatchBase match) {
158166
RouteMatchBase walker = match;
159167
while (walker is ShellRouteMatch) {
@@ -162,12 +170,14 @@ class GoRouterDelegate extends RouterDelegate<RouteMatchList>
162170
if (walker is ImperativeRouteMatch) {
163171
walker.complete(result);
164172
}
173+
174+
// Unconditionally remove the match from the current configuration
165175
currentConfiguration = currentConfiguration.remove(match);
176+
166177
notifyListeners();
167-
assert(() {
168-
_debugAssertMatchListNotEmpty();
169-
return true;
170-
}());
178+
179+
// Ensure the configuration is not empty
180+
_debugAssertMatchListNotEmpty();
171181
}
172182

173183
/// The top [GoRouterState], the state of the route that was

packages/go_router/pubspec.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
name: go_router
22
description: A declarative router for Flutter based on Navigation 2 supporting
33
deep linking, data-driven routes and more
4-
version: 14.6.0
4+
version: 14.6.1
55
repository: https://github.com/flutter/packages/tree/main/packages/go_router
66
issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+go_router%22
77

packages/go_router/test/delegate_test.dart

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,44 @@ void main() {
9292
false);
9393
});
9494

95+
testWidgets('PopScope intercepts back button on root route',
96+
(WidgetTester tester) async {
97+
bool didPop = false;
98+
99+
final GoRouter goRouter = GoRouter(
100+
initialLocation: '/',
101+
routes: <GoRoute>[
102+
GoRoute(
103+
path: '/',
104+
builder: (_, __) => PopScope(
105+
onPopInvokedWithResult: (bool result, _) {
106+
didPop = true;
107+
},
108+
canPop: false,
109+
child: const Text('Home'),
110+
),
111+
),
112+
],
113+
);
114+
115+
addTearDown(goRouter.dispose);
116+
117+
await tester.pumpWidget(MaterialApp.router(
118+
routerConfig: goRouter,
119+
));
120+
121+
expect(find.text('Home'), findsOneWidget);
122+
123+
// Simulate back button press
124+
await tester.binding.handlePopRoute();
125+
126+
await tester.pumpAndSettle();
127+
128+
// Verify that PopScope intercepted the back button
129+
expect(didPop, isTrue);
130+
expect(find.text('Home'), findsOneWidget);
131+
});
132+
95133
testWidgets('pops more than matches count should return false',
96134
(WidgetTester tester) async {
97135
final GoRouter goRouter = await createGoRouter(tester)

0 commit comments

Comments
 (0)