Skip to content

[go_router] fix: PopScope not honored for initial Routes inside ShellRoutes #8162

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions packages/go_router/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
## 14.6.2

- Fixed `PopScope`, and `WillPopScop` was not handled properly in root Shell routes.

## 14.6.1

- Fixed `PopScope`, and `WillPopScop` was not handled properly in the Root routes.
Expand All @@ -10,7 +14,6 @@

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


## 14.4.1

- Adds `missing_code_block_language_in_doc_comment` lint.
Expand All @@ -29,7 +32,7 @@

## 14.2.8

- Updated custom_stateful_shell_route example to better support swiping in TabView as well as demonstration of the use of PageView.
- Updated custom_stateful_shell_route example to better support swiping in TabView as well as demonstration of the use of PageView.

## 14.2.7

Expand Down
58 changes: 43 additions & 15 deletions packages/go_router/lib/src/delegate.dart
Original file line number Diff line number Diff line change
Expand Up @@ -55,24 +55,52 @@ class GoRouterDelegate extends RouterDelegate<RouteMatchList>

@override
Future<bool> popRoute() async {
final NavigatorState? state = _findCurrentNavigator();
if (state != null) {
final bool didPop = await state.maybePop(); // Call maybePop() directly
if (didPop) {
return true; // Return true if maybePop handled the pop
}
}
// Walk up the tree to gather the navigator states that are present.
final List<(NavigatorState, RouteBase)> navigatorStates =
<(NavigatorState, RouteBase)>[
(navigatorKey.currentState!, currentConfiguration.matches.first.route),
];

// Fallback to onExit if maybePop did not handle the pop
final GoRoute lastRoute = currentConfiguration.last.route;
if (lastRoute.onExit != null && navigatorKey.currentContext != null) {
return !(await lastRoute.onExit!(
navigatorKey.currentContext!,
currentConfiguration.last
.buildState(_configuration, currentConfiguration),
));
RouteMatchBase walker = currentConfiguration.matches.last;

while (walker is ShellRouteMatch) {
final NavigatorState potentialCandidate =
walker.navigatorKey.currentState!;

navigatorStates.add((potentialCandidate, walker.matches.last.route));

walker = walker.matches.last;
}

// Now we have a list of all the navigator states that are present in the
// current matched path. We now go through them in reverse order to check
// if they have an onExit callback and call it. If it returns false, we
// return true to indicate that the route pop was handled.
// If it returns true we proceed by trying to pop the route from the respective
// navigator. If this is successful (or prevented by doNotPop) we return true.
// If this (child) navigator cannot be popped because it has only one entry
// we continue with the next (parent) navigator.
for (final (NavigatorState navigatorState, RouteBase route)
in navigatorStates.reversed) {
// If the route has an onExit callback, call it and return false if it
// returns false.
if (route is GoRoute &&
route.onExit != null &&
!await route.onExit!(
Copy link
Contributor

Choose a reason for hiding this comment

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

why is this needed? this should be called automatically in _handlePopPageWithRouteMatch which will be called during Navigator.maybePop

Copy link
Author

@ElectricCookie ElectricCookie Nov 26, 2024

Choose a reason for hiding this comment

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

Not sure, I migrated this basically from the original implementation.

Edit: removing this code will cause on_exit_test.dart:241 to fail because returning false from the onExit method will no longer prevent popping.

navigatorKey.currentContext!,
currentConfiguration.last
.buildState(_configuration, currentConfiguration))) {
return true;
}

// Try to pop the navigator state.
final bool didPop = await navigatorState.maybePop();
if (didPop) {
return true;
}
}
// Even the final (root) navigator cannot be popped, return false to exit
// the app
return false;
}

Expand Down
2 changes: 1 addition & 1 deletion packages/go_router/pubspec.yaml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
name: go_router
description: A declarative router for Flutter based on Navigation 2 supporting
deep linking, data-driven routes and more
version: 14.6.1
version: 14.6.2
repository: https://github.com/flutter/packages/tree/main/packages/go_router
issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+go_router%22

Expand Down
73 changes: 73 additions & 0 deletions packages/go_router/test/pop_scope_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/widgets.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:go_router/go_router.dart';

import 'test_helpers.dart';

void main() {
testWidgets('back button respects PopScope', (WidgetTester tester) async {
final UniqueKey home = UniqueKey();

bool onPopCalled = false;

final List<GoRoute> routes = <GoRoute>[
GoRoute(
path: '/',
builder: (BuildContext context, GoRouterState state) => PopScope(
canPop: false,
onPopInvokedWithResult: (bool didPop, Object? result) {
onPopCalled = true;
},
child: DummyScreen(key: home),
),
),
];

final GoRouter router = await createRouter(routes, tester);

expect(await router.routerDelegate.popRoute(), true);
await tester.pumpAndSettle();

expect(onPopCalled, isTrue);

expect(find.byKey(home), findsOneWidget);
});

testWidgets('back button is respects PopScope in ShellRoute',
(WidgetTester tester) async {
bool onPopCalled = false;

final UniqueKey home = UniqueKey();

final List<RouteBase> routes = <RouteBase>[
ShellRoute(
builder: (BuildContext contex, GoRouterState state, Widget child) =>
child,
routes: <RouteBase>[
GoRoute(
path: '/',
builder: (BuildContext context, GoRouterState state) => PopScope(
canPop: false,
onPopInvokedWithResult: (bool didPop, Object? result) {
onPopCalled = true;
},
child: DummyScreen(key: home),
),
),
],
),
];

final GoRouter router = await createRouter(routes, tester);

expect(await router.routerDelegate.popRoute(), true);
await tester.pumpAndSettle();

expect(onPopCalled, isTrue);

expect(find.byKey(home), findsOneWidget);
});
}