Skip to content
Open
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
32 changes: 29 additions & 3 deletions packages/go_router/lib/src/builder.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart';

import 'configuration.dart';
Expand Down Expand Up @@ -135,6 +136,7 @@ class _CustomNavigator extends StatefulWidget {
required this.errorBuilder,
required this.errorPageBuilder,
required this.requestFocus,
this.navigatorActive,
});

final GlobalKey<NavigatorState> navigatorKey;
Expand All @@ -153,6 +155,7 @@ class _CustomNavigator extends StatefulWidget {
final GoRouterWidgetBuilder? errorBuilder;
final GoRouterPageBuilder? errorPageBuilder;
final bool requestFocus;
final ValueListenable<bool>? navigatorActive;

@override
State<StatefulWidget> createState() => _CustomNavigatorState();
Expand Down Expand Up @@ -276,8 +279,9 @@ class _CustomNavigatorState extends State<_CustomNavigator> {
ShellRouteMatch match,
RouteMatchList matchList,
List<NavigatorObserver>? observers,
String? restorationScopeId,
) {
String? restorationScopeId, {
ValueListenable<bool>? navigatorActive,
}) {
return PopScope(
// Prevent ShellRoute from being popped, for example
// by an iOS back gesture, when the route has active sub-routes.
Expand All @@ -294,6 +298,7 @@ class _CustomNavigatorState extends State<_CustomNavigator> {
configuration: widget.configuration,
observers: observers ?? const <NavigatorObserver>[],
onPopPageWithRouteMatch: widget.onPopPageWithRouteMatch,
navigatorActive: navigatorActive,
// This is used to recursively build pages under this shell route.
errorBuilder: widget.errorBuilder,
errorPageBuilder: widget.errorPageBuilder,
Expand Down Expand Up @@ -368,12 +373,15 @@ class _CustomNavigatorState extends State<_CustomNavigator> {
Page<Object?> _buildPlatformAdapterPage(BuildContext context, GoRouterState state, Widget child) {
// build the page based on app type
_cacheAppType(context);
final Widget pageChild = widget.navigatorActive == null
? child
: _BranchNavigatorPopScope(navigatorActive: widget.navigatorActive!, child: child);
return _pageBuilderForAppType!(
key: state.pageKey,
name: state.name ?? state.path,
arguments: <String, String>{...state.pathParameters, ...state.uri.queryParameters},
restorationId: state.pageKey.value,
child: child,
child: pageChild,
);
}

Expand Down Expand Up @@ -441,3 +449,21 @@ class _CustomNavigatorState extends State<_CustomNavigator> {
);
}
}

class _BranchNavigatorPopScope extends StatelessWidget {
const _BranchNavigatorPopScope({required this.navigatorActive, required this.child});

final ValueListenable<bool> navigatorActive;
final Widget child;

@override
Widget build(BuildContext context) {
return ValueListenableBuilder<bool>(
valueListenable: navigatorActive,
child: child,
builder: (BuildContext context, bool isActive, Widget? child) {
return PopScope<Object?>(canPop: isActive, child: child!);
},
);
}
}
26 changes: 21 additions & 5 deletions packages/go_router/lib/src/route.dart
Original file line number Diff line number Diff line change
Expand Up @@ -54,8 +54,9 @@ typedef NavigatorBuilder =
ShellRouteMatch match,
RouteMatchList matchList,
List<NavigatorObserver>? observers,
String? restorationScopeId,
);
String? restorationScopeId, {
ValueListenable<bool>? navigatorActive,
});

/// Signature for function used in [RouteBase.onExit].
///
Expand Down Expand Up @@ -566,8 +567,9 @@ class ShellRouteContext {
BuildContext context,
List<NavigatorObserver>? observers,
bool notifyRootObserver,
String? restorationScopeId,
) {
String? restorationScopeId, {
ValueListenable<bool>? navigatorActive,
}) {
final effectiveObservers = <NavigatorObserver>[...?observers];

if (notifyRootObserver) {
Expand All @@ -583,6 +585,7 @@ class ShellRouteContext {
routeMatchList,
effectiveObservers,
restorationScopeId,
navigatorActive: navigatorActive,
);
}
}
Expand Down Expand Up @@ -1375,12 +1378,21 @@ class StatefulNavigationShellState extends State<StatefulNavigationShell> with R
branch.observers,
route.notifyRootObserver,
branch.restorationScopeId,
navigatorActive: branchState.navigatorActive,
);
}
_updateActiveBranchNavigatorFlags();

_cleanUpObsoleteBranches();
}

void _updateActiveBranchNavigatorFlags() {
for (var i = 0; i < route.branches.length; i++) {
final _StatefulShellBranchState? branchState = _branchState[route.branches[i]];
branchState?.navigatorActive.value = i == currentIndex;
}
}

void _preloadBranches() {
for (var i = 0; i < route.branches.length; i++) {
final StatefulShellBranch branch = route.branches[i];
Expand All @@ -1398,15 +1410,17 @@ class StatefulNavigationShellState extends State<StatefulNavigationShell> with R
});
assert(match != null);

final _StatefulShellBranchState branchState = _branchStateFor(branch, false);
branchState.navigatorActive.value = false;
final Widget navigator = widget.shellRouteContext.navigatorBuilder(
branch.navigatorKey,
match!,
matchList,
branch.observers,
branch.restorationScopeId,
navigatorActive: branchState.navigatorActive,
);

final _StatefulShellBranchState branchState = _branchStateFor(branch, false);
branchState.location.value = matchList;
branchState.navigator = navigator;
}
Expand Down Expand Up @@ -1491,9 +1505,11 @@ class _StatefulShellBranchState {

Widget? navigator;
final _RestorableRouteMatchList location;
final ValueNotifier<bool> navigatorActive = ValueNotifier<bool>(false);

void dispose() {
location.dispose();
navigatorActive.dispose();
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
changelog: |
- Fixes Android system/predictive back popping inactive StatefulShellBranch navigators.
version: patch
149 changes: 149 additions & 0 deletions packages/go_router/test/stateful_shell_route_system_back_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:go_router/go_router.dart';

Expand Down Expand Up @@ -87,6 +88,93 @@ void main() {
await tester.pumpAndSettle();
expect(find.text('Home'), findsOneWidget);
});

testWidgets('does not pop inactive StatefulShellRoute branches', (WidgetTester tester) async {
final pops = <String>[];
StatefulNavigationShell? navigationShell;
addTearDown(() async {
await tester.pumpWidget(const SizedBox.shrink());
await tester.pump();
});

final GoRouter router = await createRouter(
<RouteBase>[
StatefulShellRoute.indexedStack(
builder: (BuildContext context, GoRouterState state, StatefulNavigationShell shell) {
navigationShell = shell;
return shell;
},
branches: <StatefulShellBranch>[
StatefulShellBranch(
observers: <NavigatorObserver>[_RecordingNavigatorObserver('/A', pops)],
routes: <RouteBase>[
GoRoute(
path: '/A1',
builder: (_, _) => const _BranchScreen(title: 'Stack A - 1', canPop: false),
routes: <RouteBase>[
GoRoute(
path: '/A2',
builder: (_, _) => const _BranchScreen(title: 'Stack A - 2'),
routes: <RouteBase>[
GoRoute(
path: '/A3',
builder: (_, _) => const _BranchScreen(title: 'Stack A - 3'),
),
],
),
],
),
],
),
StatefulShellBranch(
observers: <NavigatorObserver>[_RecordingNavigatorObserver('/B', pops)],
routes: <RouteBase>[
GoRoute(
path: '/B1',
builder: (_, _) => const _BranchScreen(title: 'Stack B - 1', canPop: false),
),
],
),
StatefulShellBranch(
observers: <NavigatorObserver>[_RecordingNavigatorObserver('/C', pops)],
routes: <RouteBase>[
GoRoute(
path: '/C1',
builder: (_, _) => const _BranchScreen(title: 'Stack C - 1', canPop: false),
routes: <RouteBase>[
GoRoute(
path: '/C2',
builder: (_, _) => const _BranchScreen(title: 'Stack C - 2'),
),
],
),
],
),
],
),
],
tester,
initialLocation: '/A1',
);

router.go('/A1/A2/A3');
await tester.pumpAndSettle();
expect(find.text('Stack A - 3'), findsOneWidget);

router.go('/C1/C2');
await tester.pumpAndSettle();
expect(find.text('Stack C - 2'), findsOneWidget);

navigationShell!.goBranch(1);
await tester.pumpAndSettle();
expect(find.text('Stack B - 1'), findsOneWidget);

await simulateAndroidPredictiveBackGesture(tester);
await tester.pump();

expect(find.text('Stack B - 1'), findsOneWidget);
expect(pops, isEmpty);
});
});
}

Expand Down Expand Up @@ -172,3 +260,64 @@ class _TestAppState extends State<_TestApp> {
return MaterialApp.router(routerConfig: _router);
}
}

class _BranchScreen extends StatelessWidget {
const _BranchScreen({required this.title, this.canPop = true});

final String title;
final bool canPop;

@override
Widget build(BuildContext context) {
return PopScope<Object?>(
canPop: canPop,
child: Scaffold(body: Center(child: Text(title))),
);
}
}

class _RecordingNavigatorObserver extends NavigatorObserver {
_RecordingNavigatorObserver(this.branch, this.pops);

final String branch;
final List<String> pops;

@override
void didPop(Route<dynamic> route, Route<dynamic>? previousRoute) {
pops.add('$branch ${route.settings.name} -> ${previousRoute?.settings.name}');
}
}

Future<void> simulateAndroidPredictiveBackGesture(WidgetTester tester) async {
await _handleAndroidPredictiveBackMessage(
tester,
const MethodCall('startBackGesture', <String, dynamic>{
'touchOffset': <double>[5.0, 300.0],
'progress': 0.0,
'swipeEdge': 0,
}),
);
await tester.pump();

await _handleAndroidPredictiveBackMessage(
tester,
const MethodCall('updateBackGestureProgress', <String, dynamic>{
'x': 100.0,
'y': 300.0,
'progress': 0.35,
'swipeEdge': 0,
}),
);
await tester.pump();

await _handleAndroidPredictiveBackMessage(tester, const MethodCall('commitBackGesture'));
}

Future<void> _handleAndroidPredictiveBackMessage(WidgetTester tester, MethodCall methodCall) async {
final ByteData message = const StandardMethodCodec().encodeMethodCall(methodCall);
await tester.binding.defaultBinaryMessenger.handlePlatformMessage(
'flutter/backgesture',
message,
(ByteData? _) {},
);
}