Android Predictive Back
- Summary
- Background
- Migration guide
- Migrating from WillPopScope to PopScope
- Migrating from WillPopScope to NavigatorPopHandler for nested Navigators
- Migrating from Form.onWillPop to Form.canPop and Form.onPopInvoked
- Migrating from Route.willPop to Route.popDisposition
- Migrating from ModalRoute.add/removeScopedWillPopCallback to ModalRoute.(un)registerPopInterface
- Migrating from ModalRoute.hasScopedWillPopCallback to ModalRoute.popDisposition
- Migrating a back confirmation dialog
- Supporting predictive back
- Timeline
- References
Summary
To support Android 14’s Predictive Back feature, a set of ahead-of-time APIs
have replaced just-in-time navigation APIs, like WillPopScope
and
Navigator.willPop
.
Background
Android 14 introduced the Predictive Back feature, which allows the user to peek behind the current route during a valid back gesture and decide whether to continue back or to cancel the gesture. This was incompatible with Flutter’s navigation APIs that allow the developer to cancel a back gesture after it is received.
With predictive back, the back animation begins immediately when the user initiates the gesture and before it has been committed. There is no opportunity for the Flutter app to decide whether it’s allowed to happen at that time. It must be known ahead of time.
For this reason, all APIs that allow a Flutter app developer to cancel a back navigation at the time that a back gesture is received are now deprecated. They have been replaced with equivalent APIs that maintain a boolean state at all times that dictates whether or not back navigation is possible. When it is, the predictive back animation happens as usual. Otherwise, navigation is stopped. In both cases, the app developer is informed that a back was attempted and whether or not it was successful.
PopScope
The PopScope
class directly replaces WillPopScope
. Instead of deciding
whether a pop is possible at the time it occurs, this is set ahead of time with
the canPop
boolean. You can still listen to pops by using onPopInvoked
.
PopScope(
canPop: _myPopDisableEnableLogic(),
onPopInvoked: (bool didPop) {
// Handle the pop. If `didPop` is false, it was blocked.
},
)
Form.canPop
and Form.onPopInvoked
These two new parameters are based on PopScope
and replace the deprecated
Form.onWillPop
parameter. They are used with PopScope
in the same way as
above.
Form(
canPop: _myPopDisableEnableLogic(),
onPopInvoked: (bool didPop) {
// Handle the pop. If `didPop` is false, it was blocked.
},
)
Route.popDisposition
This getter synchronously returns the RoutePopDisposition
for the route, which
describes how pops will behave.
if (myRoute.popDisposition == RoutePopDisposition.doNotPop) {
// Back gestures are disabled.
}
ModalRoute.registerPopInterface
and ModalRoute.unregisterPopInterface
Use these methods to register PopScope
widgets, to be evaluated when the route
decides whether it can pop. This functionality might be used when implementing a
custom PopScope
widget.
@override
void didChangeDependencies() {
super.didChangeDependencies();
final ModalRoute<dynamic>? nextRoute = ModalRoute.of(context);
if (nextRoute != _route) {
_route?.unregisterPopInterface(this);
_route = nextRoute;
_route?.registerPopInterface(this);
}
}
Migration guide
WillPopScope
to PopScope
Migrating from The direct replacement of the WillPopScope
widget is the PopScope
widget.
In many cases, logic that was being run at the time of the back gesture in
onWillPop
can be done at build time and set to canPop
.
Code before migration:
WillPopScope(
onWillPop: () async {
return _myCondition;
},
child: ...
),
Code after migration:
PopScope(
canPop: _myCondition,
child: ...
),
For cases where it’s necessary to be notified that a pop was attempted, the
onPopInvoked
method can be used in a similar way to onWillPop
. Keep in mind
that while onWillPop
was called before the pop was handled and had the ability
to cancel it, onPopInvoked
is called after the pop is finished being handled.
Code before migration:
WillPopScope(
onWillPop: () async {
_myHandleOnPopMethod();
return true;
},
child: ...
),
Code after migration:
PopScope(
canPop: true,
onPopInvoked (bool didPop) {
_myHandleOnPopMethod();
},
child: ...
),
WillPopScope
to NavigatorPopHandler
for nested Navigator
s
Migrating from A very common use case of WillPopScope
was to properly handle back gestures
when using nested Navigator
widgets. It’s possible to do this using PopScope
as well, but there is now a wrapper widget that makes this even easier:
NavigatorPopHandler
.
Code before migration:
WillPopScope(
onWillPop: () async => !(await _nestedNavigatorKey.currentState!.maybePop()),
child: Navigator(
key: _nestedNavigatorKey,
…
),
)
Code after migration:
NavigatorPopHandler(
onPop: () => _nestedNavigatorKey.currentState!.pop(),
child: Navigator(
key: _nestedNavigatorKey,
…
),
)
Form.onWillPop
to Form.canPop
and Form.onPopInvoked
Migrating from Previously, Form
used a WillPopScope
instance under the hood and exposed its
onWillPop
method. This has been replaced with a PopScope
that exposes its
canPop
and onPopInvoked
methods. Migrating is identical to migrating from
WillPopScope
to PopScope
, detailed above.
Route.willPop
to Route.popDisposition
Migrating from Route
’s willPop
method returned a Future<RoutePopDisposition>
to
accommodate the fact that pops could be canceled. Now that that’s no longer
true, this logic has been simplified to a synchronous getter.
Code before migration:
if (await myRoute.willPop() == RoutePopDisposition.doNotPop) {
...
}
Code after migration:
if (myRoute.popDisposition == RoutePopDisposition.doNotPop) {
...
}
ModalRoute.add/removeScopedWillPopCallback
to ModalRoute.(un)registerPopInterface
Migrating from Internally, ModalRoute
kept track of the existence of WillPopScope
s in its
widget subtree by registering them with addScopedWillPopCallback
and
removeScopedWillPopCallback
. Since PopScope
replaces WillPopScope
, these
methods have been replaced by registerPopInterface
and
unregisterPopInterface
, respectively.
PopInterface
is implemented by PopScope
in order to expose only the minimal
information necessary to ModalRoute
. Anyone writing their own PopScope
should implement PopInterface
and register and unregister their widget with
its enclosing ModalRoute
.
Code before migration:
@override
void didChangeDependencies() {
super.didChangeDependencies();
if (widget.onWillPop != null) {
_route?.removeScopedWillPopCallback(widget.onWillPop!);
}
_route = ModalRoute.of(context);
if (widget.onWillPop != null) {
_route?.addScopedWillPopCallback(widget.onWillPop!);
}
}
Code after migration:
@override
void didChangeDependencies() {
super.didChangeDependencies();
_route?.unregisterPopInterface(this);
_route = ModalRoute.of(context);
_route?.registerPopInterface(this);
}
ModalRoute.hasScopedWillPopCallback
to ModalRoute.popDisposition
Migrating from This method was previously used for a use-case very similar to Predictive Back
but in the Cupertino library, where certain back transitions allowed canceling
the navigation. The route transition was disabled when there was even the
possibility of a WillPopScope
widget canceling the pop.
Now that the API requires this to be decided ahead of time, this no longer needs
to be speculatively based on the existence of PopScope
widgets. The definitive
logic of whether a ModalRoute
is having popping blocked by a PopScope
widget
is baked into ModalRoute.popDisposition
.
Code before migration:
if (_route.hasScopedWillPopCallback) {
// Disable predictive route transitions.
}
Code after migration:
if (_route.popDisposition == RoutePopDisposition.doNotPop) {
// Disable predictive route transitions.
}
Migrating a back confirmation dialog
WillPopScope
was sometimes used to show a confirmation dialog when
a back gesture was received.
This can still be done with PopScope
in a similar pattern.
Code before migration:
WillPopScope(
onWillPop: () async {
final bool? shouldPop = await _showBackDialog();
return shouldPop ?? false;
},
child: child,
)
Code after migration:
return PopScope(
canPop: false,
onPopInvoked: (bool didPop) async {
if (didPop) {
return;
}
final NavigatorState navigator = Navigator.of(context);
final bool? shouldPop = await _showBackDialog();
if (shouldPop ?? false) {
navigator.pop();
}
},
child: child,
)
Supporting predictive back
- Run Android 33 or above.
- Enable the feature flag for predictive back on the device under “Developer options”. This will be unnecessary on future versions of Android.
- Set
android:enableOnBackInvokedCallback="true"
inandroid/app/src/main/AndroidManifest.xml
. If needed, refer to Android’s full guide for migrating Android apps to support predictive back. - Make sure you’re using version
3.14.0-7.0.pre
of Flutter or greater. - Run the app and perform a back gesture (swipe from the left side of the screen).
Timeline
Landed in version: 3.14.0-7.0.pre
In stable release: not yet
References
API documentation:
PopScope
NavigatorPopHandler
PopScope
NavigatorPopHandler
PopInterface
Form.canPop
Form.onPopInvoked
Route.popDisposition
ModalRoute.registerPopInterface
ModalRoute.unregisterPopInterface
Relevant issues:
Relevant PRs: