Skip to content
Merged
64 changes: 40 additions & 24 deletions packages/go_router/lib/src/builder.dart
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import 'package:flutter/widgets.dart';
import 'configuration.dart';
import 'logging.dart';
import 'match.dart';
import 'misc/active_branch_scope.dart';
import 'misc/error_screen.dart';
import 'misc/errors.dart';
import 'pages/cupertino.dart';
Expand Down Expand Up @@ -336,30 +337,45 @@ class _CustomNavigatorState extends State<_CustomNavigator> {
List<NavigatorObserver>? observers,
String? restorationScopeId,
) {
return PopScope(
// Prevent ShellRoute from being popped, for example
// by an iOS back gesture, when the route has active sub-routes.
// TODO(LukasMirbt): Remove when minimum flutter version includes
// https://github.com/flutter/flutter/pull/152330.
canPop: match.matches.length == 1,
child: _CustomNavigator(
// The state needs to persist across rebuild.
key: ValueKey<GlobalKey<NavigatorState>>(navigatorKey),
navigatorRestorationId: restorationScopeId,
navigatorKey: navigatorKey,
matches: match.matches,
matchList: matchList,
inheritedMetadata: state.metadata,
configuration: widget.configuration,
observers: observers ?? const <NavigatorObserver>[],
onPopPageWithRouteMatch: widget.onPopPageWithRouteMatch,
// This is used to recursively build pages under this shell route.
errorBuilder: widget.errorBuilder,
errorPageBuilder: widget.errorPageBuilder,
requestFocus: widget.requestFocus,
isShellNavigator: true,
clipBehavior: _clipBehaviorFor(match.route, navigatorKey),
),
final Widget navigator = _CustomNavigator(
// The state needs to persist across rebuild.
key: ValueKey<GlobalKey<NavigatorState>>(navigatorKey),
navigatorRestorationId: restorationScopeId,
navigatorKey: navigatorKey,
matches: match.matches,
matchList: matchList,
inheritedMetadata: state.metadata,
configuration: widget.configuration,
observers: observers ?? const <NavigatorObserver>[],
onPopPageWithRouteMatch: widget.onPopPageWithRouteMatch,
// This is used to recursively build pages under this shell route.
errorBuilder: widget.errorBuilder,
errorPageBuilder: widget.errorPageBuilder,
requestFocus: widget.requestFocus,
isShellNavigator: true,
clipBehavior: _clipBehaviorFor(match.route, navigatorKey),
);
// The Builder gives the PopScope a context below the
// ActiveBranchScope of the branch this Navigator belongs to, so
// that canPop is kept up to date when the active branch changes.
return Builder(
builder: (BuildContext context) {
return PopScope(
// Prevent ShellRoute from being popped, for example
// by an iOS back gesture, when the route has active
// sub-routes.
//
// Inactive StatefulShellRoute branches stay mounted, and
// their PopScope is registered on the same enclosing route
// as the one of the active branch. Since a single doNotPop
// vote suppresses the pop for all of them, only the active
// branch may hold back a pop.
// TODO(LukasMirbt): Remove when minimum flutter version includes
// https://github.com/flutter/flutter/pull/152330.
canPop: match.matches.length == 1 || !ActiveBranchScope.isActiveBranchOf(context),
child: navigator,
);
},
);
},
);
Expand Down
36 changes: 36 additions & 0 deletions packages/go_router/lib/src/misc/active_branch_scope.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
// Copyright 2013 The Flutter Authors
// 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:meta/meta.dart';

/// Marks whether the subtree below it belongs to the currently active branch
/// of a `StatefulShellRoute`.
///
/// Every branch of a `StatefulShellRoute` stays mounted, including the
/// inactive ones. Widgets that must only take effect for the branch the user
/// is currently looking at can use [isActiveBranchOf] to find out whether they
/// are part of the active branch.
@internal
class ActiveBranchScope extends InheritedWidget {
Comment thread
IbrarIlyas marked this conversation as resolved.
Resolved
/// Constructs an [ActiveBranchScope].
const ActiveBranchScope({required this.isActive, required super.child, super.key});

/// Whether the subtree below this widget belongs to the active branch of the
/// enclosing `StatefulShellRoute`.
final bool isActive;

/// Whether [context] is part of the active branch of the closest enclosing
/// `StatefulShellRoute`.
///
/// Returns true when [context] is not below a `StatefulShellRoute`, for
/// instance for the [Navigator] of a plain `ShellRoute`.
///
/// The calling widget is rebuilt whenever the active branch changes.
static bool isActiveBranchOf(BuildContext context) =>
context.dependOnInheritedWidgetOfExactType<ActiveBranchScope>()?.isActive ?? true;

@override
bool updateShouldNotify(ActiveBranchScope oldWidget) => isActive != oldWidget.isActive;
}
24 changes: 20 additions & 4 deletions packages/go_router/lib/src/route.dart
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import 'package:meta/meta.dart' as meta;

import 'configuration.dart';
import 'match.dart';
import 'misc/active_branch_scope.dart';
import 'path_utils.dart';
import 'router.dart';
import 'state.dart';
Expand Down Expand Up @@ -1635,13 +1636,17 @@ class StatefulNavigationShellState extends State<StatefulNavigationShell> with R

@override
Widget build(BuildContext context) {
// A nested StatefulShellRoute is only active if the branch hosting it is
// active as well.
final bool isActiveShell = ActiveBranchScope.isActiveBranchOf(context);
final List<Widget> children = route.branches
.map(
(StatefulShellBranch branch) => _BranchNavigatorProxy(
.mapIndexed(
(int index, StatefulShellBranch branch) => _BranchNavigatorProxy(
key: ValueKey<GlobalKey<NavigatorState>>(branch.navigatorKey),
branch: branch,
navigatorForBranch: (StatefulShellBranch branch) =>
_branchState[branch.navigatorKey]?.navigator,
isActive: isActiveShell && index == currentIndex,
),
)
.toList();
Expand Down Expand Up @@ -1712,11 +1717,19 @@ typedef _NavigatorForBranch = Widget? Function(StatefulShellBranch);
/// important for container implementations that cache child widgets,
/// such as [TabBarView].
class _BranchNavigatorProxy extends StatefulWidget {
const _BranchNavigatorProxy({super.key, required this.branch, required this.navigatorForBranch});
const _BranchNavigatorProxy({
super.key,
required this.branch,
required this.navigatorForBranch,
required this.isActive,
});

final StatefulShellBranch branch;
final _NavigatorForBranch navigatorForBranch;

/// Whether [branch] is the currently active branch.
final bool isActive;

@override
State<StatefulWidget> createState() => _BranchNavigatorProxyState();
}
Expand All @@ -1729,7 +1742,10 @@ class _BranchNavigatorProxyState extends State<_BranchNavigatorProxy>
@override
Widget build(BuildContext context) {
super.build(context);
return widget.navigatorForBranch(widget.branch) ?? const SizedBox.shrink();
return ActiveBranchScope(
isActive: widget.isActive,
child: widget.navigatorForBranch(widget.branch) ?? const SizedBox.shrink(),
);
}

@override
Expand Down
3 changes: 3 additions & 0 deletions packages/go_router/pending_changelogs/fix_issue_193098.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
changelog: |
- Fixes the system back button being ignored after switching away from a `StatefulShellRoute` branch that still has an active sub-route.
version: patch
Loading
Loading