-
Notifications
You must be signed in to change notification settings - Fork 3.5k
Migration guide for predictive back #8952
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 2 commits
0aafccc
5f19de4
e90c97c
ec56e96
a8aa178
bc2f56e
f16aeb6
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,314 @@ | ||||||||||||||||||||
| --- | ||||||||||||||||||||
| title: Android Predictive Back | ||||||||||||||||||||
| description: > | ||||||||||||||||||||
| The ability to control back navigation at the time that a back gesture is | ||||||||||||||||||||
| received has been replaced with an ahead-of-time navigation API in order to | ||||||||||||||||||||
| support Android 14's Predictive Back feature. | ||||||||||||||||||||
| --- | ||||||||||||||||||||
|
|
||||||||||||||||||||
| ## Summary | ||||||||||||||||||||
|
|
||||||||||||||||||||
| Flutter's just-in-time navigation APIs, like `WillPopScope` and | ||||||||||||||||||||
| `Navigator.willPop`, are being replaced with a set of ahead-of-time APIs in | ||||||||||||||||||||
| order to support Android 14's Predictive Back feature. | ||||||||||||||||||||
|
|
||||||||||||||||||||
| ## Background | ||||||||||||||||||||
|
|
||||||||||||||||||||
| Android 14 introduced the | ||||||||||||||||||||
| [Predictive Back feature](https://developer.android.com/guide/navigation/predictive-back-gesture), | ||||||||||||||||||||
| which allows the user to peek behind the current route during a valid back | ||||||||||||||||||||
| gesture and decide whether or not to continue back or to cancel the gesture. | ||||||||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||||||||||||
| 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 or not it is allowed to happen | ||||||||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||||||||||||
| 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` | ||||||||||||||||||||
| `PopScope` is a direct replacement for `WillPopScope`. Instead of deciding | ||||||||||||||||||||
| whether or not a pop is possible at the time it occurs, this is set ahead of | ||||||||||||||||||||
| time with the `canPop` boolean. It's also still possible to listen to pops by | ||||||||||||||||||||
| using `onPopInvoked`. | ||||||||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||||||||||||
|
|
||||||||||||||||||||
| ```dart | ||||||||||||||||||||
| PopScope( | ||||||||||||||||||||
| canPop: _myPopDisableEnableLogic(), | ||||||||||||||||||||
| onPopInvoked: (bool didPop) { | ||||||||||||||||||||
| // Handle the pop. If `didPop` is false, it was blocked. | ||||||||||||||||||||
| }, | ||||||||||||||||||||
| ) | ||||||||||||||||||||
| ``` | ||||||||||||||||||||
|
|
||||||||||||||||||||
| ### `Form.canPop` and `Form.onPopInvoked` | ||||||||||||||||||||
| These replace parameters that corresponded with `WillPopScope` and now are used | ||||||||||||||||||||
| with `PopScope` in the same was as above. | ||||||||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fields? Properties? Methods? It seems like one is a property and the other is a method?
Suggested change
|
||||||||||||||||||||
|
|
||||||||||||||||||||
| ```dart | ||||||||||||||||||||
| 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. | ||||||||||||||||||||
|
|
||||||||||||||||||||
| ```dart | ||||||||||||||||||||
| if (myRoute.popDisposition == RoutePopDisposition.doNotPop) { | ||||||||||||||||||||
| // Back gestures are disabled. | ||||||||||||||||||||
| } | ||||||||||||||||||||
| ``` | ||||||||||||||||||||
|
|
||||||||||||||||||||
| ### `ModalRoute.registerPopInterface` and `ModalRoute.unregisterPopInterface` | ||||||||||||||||||||
| These are used internally to register `PopScope` widgets, so that they are taken | ||||||||||||||||||||
| into consideration when the route decides whether or not it can pop. This may be | ||||||||||||||||||||
| used if implementing a custom `PopScope` widget. | ||||||||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||||||||||||
|
|
||||||||||||||||||||
| ```dart | ||||||||||||||||||||
| @override | ||||||||||||||||||||
| void didChangeDependencies() { | ||||||||||||||||||||
| super.didChangeDependencies(); | ||||||||||||||||||||
| _route?.unregisterPopInterface(this); | ||||||||||||||||||||
| _route = ModalRoute.of(context); | ||||||||||||||||||||
| _route?.registerPopInterface(this); | ||||||||||||||||||||
| } | ||||||||||||||||||||
| ``` | ||||||||||||||||||||
|
|
||||||||||||||||||||
| ## Migration guide | ||||||||||||||||||||
|
|
||||||||||||||||||||
| ### Migrating from `WillPopScope` to `PopScope` | ||||||||||||||||||||
| 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: | ||||||||||||||||||||
|
|
||||||||||||||||||||
| ```dart | ||||||||||||||||||||
| WillPopScope( | ||||||||||||||||||||
| onWillPop: () async { | ||||||||||||||||||||
| return _myCondition; | ||||||||||||||||||||
| }, | ||||||||||||||||||||
| child: ... | ||||||||||||||||||||
| ), | ||||||||||||||||||||
| ``` | ||||||||||||||||||||
|
|
||||||||||||||||||||
| Code after migration: | ||||||||||||||||||||
|
|
||||||||||||||||||||
| ```dart | ||||||||||||||||||||
| 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: | ||||||||||||||||||||
|
|
||||||||||||||||||||
| ```dart | ||||||||||||||||||||
| WillPopScope( | ||||||||||||||||||||
| onWillPop: () async { | ||||||||||||||||||||
| _myHandleOnPopMethod(); | ||||||||||||||||||||
| return true; | ||||||||||||||||||||
| }, | ||||||||||||||||||||
| child: ... | ||||||||||||||||||||
| ), | ||||||||||||||||||||
| ``` | ||||||||||||||||||||
|
|
||||||||||||||||||||
| Code after migration: | ||||||||||||||||||||
|
|
||||||||||||||||||||
| ```dart | ||||||||||||||||||||
| PopScope( | ||||||||||||||||||||
| canPop: true, | ||||||||||||||||||||
| onPopInvoked (bool didPop) { | ||||||||||||||||||||
| _myHandleOnPopMethod(); | ||||||||||||||||||||
| }, | ||||||||||||||||||||
| child: ... | ||||||||||||||||||||
| ), | ||||||||||||||||||||
| ``` | ||||||||||||||||||||
|
|
||||||||||||||||||||
| ### Migrating from `WillPopScope` to `NavigatorPopHandler` for nested `Navigator`s | ||||||||||||||||||||
| 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: | ||||||||||||||||||||
|
|
||||||||||||||||||||
| ```dart | ||||||||||||||||||||
| WillPopScope( | ||||||||||||||||||||
| onWillPop: () async => !(await _nestedNavigatorKey.currentState!.maybePop()), | ||||||||||||||||||||
| child: Navigator( | ||||||||||||||||||||
| key: _nestedNavigatorKey, | ||||||||||||||||||||
| … | ||||||||||||||||||||
| ), | ||||||||||||||||||||
| ) | ||||||||||||||||||||
| ``` | ||||||||||||||||||||
|
|
||||||||||||||||||||
| Code after migration: | ||||||||||||||||||||
|
|
||||||||||||||||||||
| ```dart | ||||||||||||||||||||
| NavigatorPopHandler( | ||||||||||||||||||||
| onPop: () => _nestedNavigatorKey.currentState!.pop(), | ||||||||||||||||||||
| child: Navigator( | ||||||||||||||||||||
| key: _nestedNavigatorKey, | ||||||||||||||||||||
| … | ||||||||||||||||||||
| ), | ||||||||||||||||||||
| ) | ||||||||||||||||||||
| ``` | ||||||||||||||||||||
|
|
||||||||||||||||||||
| ### Migrating from `Form.onWillPop` to `Form.canPop` and `Form.onPopInvoked` | ||||||||||||||||||||
| `Form` used to use a `WillPopScope` under the hood and expose its `onWillPop` | ||||||||||||||||||||
| method. It has been replaced with a `PopScope` and has exposed its `canPop` and | ||||||||||||||||||||
| `onPopInvoked` methods. Migrating is identical to migrating from `WillPopScope` | ||||||||||||||||||||
| to `PopScope`, detailed above. | ||||||||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||||||||||||
|
|
||||||||||||||||||||
| ### Migrating from `Route.willPop` to `Route.popDisposition` | ||||||||||||||||||||
| `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: | ||||||||||||||||||||
|
|
||||||||||||||||||||
| ```dart | ||||||||||||||||||||
| if (await myRoute.willPop() == RoutePopDisposition.doNotPop) { | ||||||||||||||||||||
| ... | ||||||||||||||||||||
| } | ||||||||||||||||||||
| ``` | ||||||||||||||||||||
|
|
||||||||||||||||||||
| Code after migration: | ||||||||||||||||||||
|
|
||||||||||||||||||||
| ```dart | ||||||||||||||||||||
| if (myRoute.popDisposition == RoutePopDisposition.doNotPop) { | ||||||||||||||||||||
| ... | ||||||||||||||||||||
| } | ||||||||||||||||||||
| ``` | ||||||||||||||||||||
|
|
||||||||||||||||||||
| ### Migrating from `ModalRoute.add/removeScopedWillPopCallback` to `ModalRoute.(un)registerPopInterface` | ||||||||||||||||||||
| Internally, `ModalRoute` kept track of the existence of `WillPopScope`s in its | ||||||||||||||||||||
| widget subtree by registering them with `addScopedWillPopCallback` and | ||||||||||||||||||||
| `removeScopedWillPopCallback`. Since `WillPopScope` has been replaced by | ||||||||||||||||||||
| `PopScope`, these methods have been replaced by `registerPopInterface` and | ||||||||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||||||||||||
| `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: | ||||||||||||||||||||
|
|
||||||||||||||||||||
| ```dart | ||||||||||||||||||||
| @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: | ||||||||||||||||||||
|
|
||||||||||||||||||||
| ```dart | ||||||||||||||||||||
| @override | ||||||||||||||||||||
| void didChangeDependencies() { | ||||||||||||||||||||
| super.didChangeDependencies(); | ||||||||||||||||||||
| _route?.unregisterPopInterface(this); | ||||||||||||||||||||
| _route = ModalRoute.of(context); | ||||||||||||||||||||
| _route?.registerPopInterface(this); | ||||||||||||||||||||
| } | ||||||||||||||||||||
| ``` | ||||||||||||||||||||
|
|
||||||||||||||||||||
| ### Migrating from `ModalRoute.hasScopedWillPopCallback` to `ModalRoute.popDisposition` | ||||||||||||||||||||
| 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: | ||||||||||||||||||||
|
|
||||||||||||||||||||
| ```dart | ||||||||||||||||||||
| if (_route.hasScopedWillPopCallback) { | ||||||||||||||||||||
| // Disable predictive route transitions. | ||||||||||||||||||||
| } | ||||||||||||||||||||
| ``` | ||||||||||||||||||||
|
|
||||||||||||||||||||
| Code after migration: | ||||||||||||||||||||
|
|
||||||||||||||||||||
| ```dart | ||||||||||||||||||||
| if (_route.popDisposition == RoutePopDisposition.doNotPop) { | ||||||||||||||||||||
| // Disable predictive route transitions. | ||||||||||||||||||||
| } | ||||||||||||||||||||
| ``` | ||||||||||||||||||||
|
|
||||||||||||||||||||
| ## Timeline | ||||||||||||||||||||
|
|
||||||||||||||||||||
| Landed in version: xxx<br> | ||||||||||||||||||||
| In stable release: not yet | ||||||||||||||||||||
|
|
||||||||||||||||||||
| ## References | ||||||||||||||||||||
|
|
||||||||||||||||||||
| {% include docs/master-api.md %} | ||||||||||||||||||||
|
|
||||||||||||||||||||
| API documentation: | ||||||||||||||||||||
|
|
||||||||||||||||||||
| * [`PopScope`][] | ||||||||||||||||||||
| * [`NavigatorPopHandler`][] | ||||||||||||||||||||
| * [`PopScope`][] | ||||||||||||||||||||
| * [`NavigatorPopHandler`][] | ||||||||||||||||||||
| * [`PopInterface`][] | ||||||||||||||||||||
| * [`Form.canPop`][] | ||||||||||||||||||||
| * [`Form.onPopInvoked`][] | ||||||||||||||||||||
| * [`Route.popDisposition`][] | ||||||||||||||||||||
| * [`ModalRoute.registerPopInterface`][] | ||||||||||||||||||||
| * [`ModalRoute.unregisterPopInterface`][] | ||||||||||||||||||||
|
|
||||||||||||||||||||
| Relevant issues: | ||||||||||||||||||||
|
|
||||||||||||||||||||
| * [Issue 109513][] | ||||||||||||||||||||
|
|
||||||||||||||||||||
| Relevant PRs: | ||||||||||||||||||||
|
|
||||||||||||||||||||
| * [Predictive Back support for root routes][] | ||||||||||||||||||||
| * [Platform channel for predictive back][] | ||||||||||||||||||||
|
|
||||||||||||||||||||
| {% include docs/master-api.md %} | ||||||||||||||||||||
|
|
||||||||||||||||||||
| [`PopScope`]: {{site.master-api}}/flutter/widgets/PopScope-class.html | ||||||||||||||||||||
| [`NavigatorPopHandler`]: {{site.master-api}}/flutter/widgets/NavigatorPopHandler-class.html | ||||||||||||||||||||
| [`PopInterface`]: {{site.master-api}}/flutter/widgets/PopInterface-class.html | ||||||||||||||||||||
| [`Form.canPop`]: {{site.master-api}}/flutter/widgets/Form/canPop.html | ||||||||||||||||||||
| [`Form.onPopInvoked`]: {{site.master-api}}/flutter/widgets/Form/onPopInvoked.html | ||||||||||||||||||||
| [`Route.popDisposition`]: {{site.master-api}}/flutter/widgets/Route/popDisposition.html | ||||||||||||||||||||
| [`ModalRoute.registerPopInterface`]: {{site.master-api}}/flutter/widgets/ModalRoute/registerPopInterface.html | ||||||||||||||||||||
| [`ModalRoute.unregisterPopInterface`]: {{site.master-api}}/flutter/widgets/ModalRoute/unregisterPopInterface.html | ||||||||||||||||||||
|
|
||||||||||||||||||||
| [Issue 109513]: {{site.repo.flutter}}/issues/109513 | ||||||||||||||||||||
| [Predictive back support for root routes]: {{site.repo.flutter}}/pull/120385 | ||||||||||||||||||||
| [Platform channel for predictive back]: {{site.repo.engine}}/pull/39208 | ||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.