Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 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
4 changes: 4 additions & 0 deletions packages/go_router/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
## 15.1.3

- Fixes calling `PopScope.onPopInvokedWithResult` in branch routes.

## 15.1.2

- Fixes focus request propagation from `GoRouter` to `Navigator` by properly handling the `requestFocus` parameter.
Expand Down
35 changes: 21 additions & 14 deletions packages/go_router/lib/src/delegate.dart
Original file line number Diff line number Diff line change
Expand Up @@ -55,8 +55,8 @@ class GoRouterDelegate extends RouterDelegate<RouteMatchList>

@override
Future<bool> popRoute() async {
final NavigatorState? state = _findCurrentNavigator();
if (state != null) {
final List<NavigatorState> states = _findCurrentNavigators();
for (final NavigatorState state in states) {
final bool didPop = await state.maybePop(); // Call maybePop() directly
if (didPop) {
return true; // Return true if maybePop handled the pop
Expand Down Expand Up @@ -96,17 +96,27 @@ class GoRouterDelegate extends RouterDelegate<RouteMatchList>

/// Pops the top-most route.
void pop<T extends Object?>([T? result]) {
final NavigatorState? state = _findCurrentNavigator();
if (state == null || !state.canPop()) {
final Iterable<NavigatorState> states = _findCurrentNavigators().where(
(NavigatorState element) => element.canPop(),
);
if (states.isEmpty) {
throw GoError('There is nothing to pop');
}
state.pop(result);
states.first.pop(result);
}

NavigatorState? _findCurrentNavigator() {
NavigatorState? state;
state =
navigatorKey.currentState; // Set state directly without canPop check
/// Get a prioritized list of NavigatorStates,
/// which either can pop or are exit routes.
///
/// 1. Sub route within branches of shell navigation
/// 2. Branch route
/// 3. Parent route
List<NavigatorState> _findCurrentNavigators() {
final List<NavigatorState> states = <NavigatorState>[];
if (navigatorKey.currentState != null) {
// Set state directly without canPop check
states.add(navigatorKey.currentState!);
}

RouteMatchBase walker = currentConfiguration.matches.last;
while (walker is ShellRouteMatch) {
Expand All @@ -119,13 +129,10 @@ class GoRouterDelegate extends RouterDelegate<RouteMatchList>
// Stop if there is a pageless route on top of the shell route.
break;
}

if (potentialCandidate.canPop()) {
state = walker.navigatorKey.currentState;
}
states.insert(0, potentialCandidate);
Copy link
Contributor

Choose a reason for hiding this comment

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

you can do a reversed at the end

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thanks, I updated the code to use reversed and iterable as return type.

walker = walker.matches.last;
}
return state;
return states;
}

bool _handlePopPageWithRouteMatch(
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: 15.1.2
version: 15.1.3
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
136 changes: 136 additions & 0 deletions packages/go_router/test/delegate_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,57 @@ Future<GoRouter> createGoRouterWithStatefulShellRoute(
return router;
}

Future<GoRouter> createGoRouterWithStatefulShellRouteAndPopScopes(
WidgetTester tester, {
bool canPopShellRouteBuilder = true,
bool canPopBranch = true,
bool canPopBranchSubRoute = true,
PopInvokedWithResultCallback<bool>? onPopShellRouteBuilder,
PopInvokedWithResultCallback<bool>? onPopBranch,
PopInvokedWithResultCallback<bool>? onPopBranchSubRoute,
}) async {
final GoRouter router = GoRouter(
initialLocation: '/c',
routes: <RouteBase>[
StatefulShellRoute.indexedStack(
branches: <StatefulShellBranch>[
StatefulShellBranch(routes: <RouteBase>[
GoRoute(
path: '/c',
builder: (_, __) => PopScope(
onPopInvokedWithResult: onPopBranch,
canPop: canPopBranch,
child: const Text('Home')),
routes: <RouteBase>[
GoRoute(
path: 'c1',
builder: (_, __) => PopScope(
onPopInvokedWithResult: onPopBranchSubRoute,
canPop: canPopBranchSubRoute,
child: const Text('SubRoute'),
),
),
]),
]),
],
builder: (BuildContext context, GoRouterState state,
StatefulNavigationShell navigationShell) =>
PopScope(
onPopInvokedWithResult: onPopShellRouteBuilder,
canPop: canPopShellRouteBuilder,
child: navigationShell,
),
),
],
);

addTearDown(router.dispose);
await tester.pumpWidget(MaterialApp.router(
routerConfig: router,
));
return router;
}

void main() {
group('pop', () {
testWidgets('removes the last element', (WidgetTester tester) async {
Expand Down Expand Up @@ -130,6 +181,91 @@ void main() {
expect(find.text('Home'), findsOneWidget);
});

testWidgets(
'PopScope intercepts back button on StatefulShellRoute builder route',
(WidgetTester tester) async {
bool didPopShellRouteBuilder = false;
bool didPopBranch = false;
bool didPopBranchSubRoute = false;

await createGoRouterWithStatefulShellRouteAndPopScopes(
tester,
canPopShellRouteBuilder: false,
onPopShellRouteBuilder: (_, __) => didPopShellRouteBuilder = true,
onPopBranch: (_, __) => didPopBranch = true,
onPopBranchSubRoute: (_, __) => didPopBranchSubRoute = true,
);

expect(find.text('Home'), findsOneWidget);
await tester.binding.handlePopRoute();
await tester.pumpAndSettle();

// Verify that PopScope intercepted the back button
expect(didPopShellRouteBuilder, isTrue);
expect(didPopBranch, isFalse);
expect(didPopBranchSubRoute, isFalse);

expect(find.text('Home'), findsOneWidget);
});

testWidgets(
'PopScope intercepts back button on StatefulShellRoute branch route',
(WidgetTester tester) async {
bool didPopShellRouteBuilder = false;
bool didPopBranch = false;
bool didPopBranchSubRoute = false;

await createGoRouterWithStatefulShellRouteAndPopScopes(
tester,
canPopBranch: false,
onPopShellRouteBuilder: (_, __) => didPopShellRouteBuilder = true,
onPopBranch: (_, __) => didPopBranch = true,
onPopBranchSubRoute: (_, __) => didPopBranchSubRoute = true,
);

expect(find.text('Home'), findsOneWidget);
await tester.binding.handlePopRoute();
await tester.pumpAndSettle();

// Verify that PopScope intercepted the back button
expect(didPopShellRouteBuilder, isFalse);
expect(didPopBranch, isTrue);
expect(didPopBranchSubRoute, isFalse);

expect(find.text('Home'), findsOneWidget);
});

testWidgets(
'PopScope intercepts back button on StatefulShellRoute branch sub route',
(WidgetTester tester) async {
bool didPopShellRouteBuilder = false;
bool didPopBranch = false;
bool didPopBranchSubRoute = false;

final GoRouter goRouter =
await createGoRouterWithStatefulShellRouteAndPopScopes(
tester,
canPopBranchSubRoute: false,
onPopShellRouteBuilder: (_, __) => didPopShellRouteBuilder = true,
onPopBranch: (_, __) => didPopBranch = true,
onPopBranchSubRoute: (_, __) => didPopBranchSubRoute = true,
);

goRouter.push('/c/c1');
await tester.pumpAndSettle();

expect(find.text('SubRoute'), findsOneWidget);
await tester.binding.handlePopRoute();
await tester.pumpAndSettle();

// Verify that PopScope intercepted the back button
expect(didPopShellRouteBuilder, isFalse);
expect(didPopBranch, isFalse);
expect(didPopBranchSubRoute, isTrue);

expect(find.text('SubRoute'), findsOneWidget);
});

testWidgets('pops more than matches count should return false',
(WidgetTester tester) async {
final GoRouter goRouter = await createGoRouter(tester)
Expand Down