Will pop scope on home route (#152057)

Fixes a bug where WillPopScope no longer worked on the home route.

With this PR, Android's predictive back feature will be explicitly disabled when a WillPopScope widget is in the widget tree. To get the same behavior and still support predictive back, use PopScope.
This commit is contained in:
Justin McCandless 2024-07-22 10:06:55 -07:00 committed by GitHub
parent c9ee9ad616
commit bb8f6b04d5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 81 additions and 2 deletions

View File

@ -3613,7 +3613,7 @@ class NavigatorState extends State<Navigator> with TickerProviderStateMixin, Res
void _handleHistoryChanged() {
final bool navigatorCanPop = canPop();
late final bool routeBlocksPop;
final bool routeBlocksPop;
if (!navigatorCanPop) {
final _RouteEntry? lastEntry = _lastRouteEntryWhereOrNull(_RouteEntry.isPresentPredicate);
routeBlocksPop = lastEntry != null

View File

@ -1807,6 +1807,9 @@ abstract class ModalRoute<T> extends TransitionRoute<T> with LocalHistoryRoute<T
void addScopedWillPopCallback(WillPopCallback callback) {
assert(_scopeKey.currentState != null, 'Tried to add a willPop callback to a route that is not currently in the tree.');
_willPopCallbacks.add(callback);
if (_willPopCallbacks.length == 1) {
_maybeDispatchNavigationNotification();
}
}
/// Remove one of the callbacks run by [willPop].
@ -1823,6 +1826,9 @@ abstract class ModalRoute<T> extends TransitionRoute<T> with LocalHistoryRoute<T
void removeScopedWillPopCallback(WillPopCallback callback) {
assert(_scopeKey.currentState != null, 'Tried to remove a willPop callback from a route that is not currently in the tree.');
_willPopCallbacks.remove(callback);
if (_willPopCallbacks.isEmpty) {
_maybeDispatchNavigationNotification();
}
}
/// Registers the existence of a [PopEntry] in the route.
@ -1860,7 +1866,8 @@ abstract class ModalRoute<T> extends TransitionRoute<T> with LocalHistoryRoute<T
// canPop indicates that the originator of the Notification can handle a
// pop. In the case of PopScope, it handles pops when canPop is
// false. Hence the seemingly backward logic here.
canHandlePop: popDisposition == RoutePopDisposition.doNotPop,
canHandlePop: popDisposition == RoutePopDisposition.doNotPop
|| _willPopCallbacks.isNotEmpty,
);
// Avoid dispatching a notification in the middle of a build.
switch (SchedulerBinding.instance.schedulerPhase) {

View File

@ -1971,6 +1971,78 @@ void main() {
await tester.restoreFrom(restorationData);
expect(find.byType(AlertDialog), findsOneWidget);
}, skip: isBrowser); // https://github.com/flutter/flutter/issues/33615
group('NavigationNotifications', () {
testWidgets('with no WillPopScope', (WidgetTester tester) async {
final List<NavigationNotification> notifications = <NavigationNotification>[];
await tester.pumpWidget(
NotificationListener<NavigationNotification>(
onNotification: (NavigationNotification notification) {
notifications.add(notification);
return true;
},
child: Directionality(
textDirection: TextDirection.ltr,
child: Navigator(
initialRoute: '/',
onGenerateRoute: (RouteSettings settings) {
return MaterialPageRoute<void>(
builder: (BuildContext context) {
return const SizedBox.shrink();
},
settings: settings,
);
},
),
),
),
);
// Only one notification, from the initial route, where a pop can't be
// handled because there's no other route to pop.
expect(notifications, hasLength(1));
expect(notifications.first.canHandlePop, isFalse);
});
testWidgets('with WillPopScope', (WidgetTester tester) async {
final List<NavigationNotification> notifications = <NavigationNotification>[];
await tester.pumpWidget(
NotificationListener<NavigationNotification>(
onNotification: (NavigationNotification notification) {
notifications.add(notification);
return true;
},
child: Directionality(
textDirection: TextDirection.ltr,
child: Navigator(
initialRoute: '/',
onGenerateRoute: (RouteSettings settings) {
return MaterialPageRoute<void>(
builder: (BuildContext context) {
return WillPopScope(
onWillPop: () {
return Future<bool>.value(false);
},
child: const SizedBox.shrink(),
);
},
settings: settings,
);
},
),
),
),
);
// Two notifications. The first is from the initial route, where a pop
// can't be handled because it's the only route. The second is from
// registering the WillPopScope, where it will always want to receive
// pops.
expect(notifications, hasLength(2));
expect(notifications.first.canHandlePop, isFalse);
expect(notifications.last.canHandlePop, isTrue);
});
});
}
double _getOpacity(GlobalKey key, WidgetTester tester) {