Skip to content
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

Migration guide for predictive back #8952

Merged
merged 7 commits into from
Aug 7, 2023
Merged

Conversation

justinmc
Copy link
Contributor

@justinmc justinmc commented Jun 27, 2023

Migration guide for Android 14's Predictive Back feature, which deprecates and replaces several navigation APIs.

Related PRs:
flutter/flutter#120385
flutter/engine#39208

TODOs

Presubmit checklist

Copy link
Contributor

@sfshaza2 sfshaza2 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM with some edits. Once it's certain that it's going into the release, it can be merged into the 3.13 branch. I'm also adding the appropriate label.


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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
opportunity for the Flutter app to decide whether or not it is allowed to happen
opportunity for the Flutter app to decide whether it's allowed to happen

Comment on lines 53 to 54
These replace parameters that corresponded with `WillPopScope` and now are used
with `PopScope` in the same was as above.
Copy link
Contributor

Choose a reason for hiding this comment

The 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
These replace parameters that corresponded with `WillPopScope` and now are used
with `PopScope` in the same was as above.
These fields replace parameters that corresponded with `WillPopScope`
and are now used with `PopScope` in the same way as above.

Comment on lines 76 to 78
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.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
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.
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.

Comment on lines 38 to 41
`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`.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
`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`.
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`.

Comment on lines 11 to 13
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.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
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.
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`.

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.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
gesture and decide whether or not to continue back or to cancel the gesture.
gesture and decide whether to continue back or to cancel the gesture.

Comment on lines 177 to 180
`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.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
`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.
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.

Comment on lines 206 to 207
`removeScopedWillPopCallback`. Since `WillPopScope` has been replaced by
`PopScope`, these methods have been replaced by `registerPopInterface` and
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
`removeScopedWillPopCallback`. Since `WillPopScope` has been replaced by
`PopScope`, these methods have been replaced by `registerPopInterface` and
`removeScopedWillPopCallback`. Since `PopScope` replaces `WillPopScope`,
these methods have been replaced by `registerPopInterface` and

@sfshaza2 sfshaza2 added the act.merge-to-next Merge PR to next stable branch release label Jul 31, 2023
@justinmc
Copy link
Contributor Author

justinmc commented Aug 1, 2023

Thanks! Changes made. I'll plan to merge this after the flutter/flutter PR goes in.

auto-submit bot pushed a commit to flutter/flutter that referenced this pull request Aug 4, 2023
This PR aims to support Android's predictive back gesture when popping the entire Flutter app.  Predictive route transitions between routes inside of a Flutter app will come later.

<img width="200" src="https://user-images.githubusercontent.com/389558/217918109-945febaa-9086-41cc-a476-1a189c7831d8.gif" />

### Trying it out

If you want to try this feature yourself, here are the necessary steps:

  1. Run Android 33 or above.
  1. Enable the feature flag for predictive back on the device under "Developer
     options".
  1. Create a Flutter project, or clone [my example project](https://github.com/justinmc/flutter_predictive_back_examples).
  1. Set `android:enableOnBackInvokedCallback="true"` in
     android/app/src/main/AndroidManifest.xml (already done in the example project).
  1. Check out this branch.
  1. Run the app. Perform a back gesture (swipe from the left side of the
     screen).

You should see the predictive back animation like in the animation above and be able to commit or cancel it.

### go_router support

go_router works with predictive back out of the box because it uses a Navigator internally that dispatches NavigationNotifications!

~~go_router can be supported by adding a listener to the router and updating SystemNavigator.setFrameworkHandlesBack.~~

Similar to with nested Navigators, nested go_routers is supported by using a PopScope widget.

<details>

<summary>Full example of nested go_routers</summary>

```dart
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'package:go_router/go_router.dart';

import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';

void main() => runApp(_MyApp());

class _MyApp extends StatelessWidget {
  final GoRouter router = GoRouter(
    routes: <RouteBase>[
      GoRoute(
        path: '/',
        builder: (BuildContext context, GoRouterState state) => _HomePage(),
      ),
      GoRoute(
        path: '/nested_navigators',
        builder: (BuildContext context, GoRouterState state) => _NestedGoRoutersPage(),
      ),
    ],
  );

  @OverRide
  Widget build(BuildContext context) {
    return MaterialApp.router(
      routerConfig: router,
    );
  }
}

class _HomePage extends StatelessWidget {
  @OverRide
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Nested Navigators Example'),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            const Text('Home Page'),
            const Text('A system back gesture here will exit the app.'),
            const SizedBox(height: 20.0),
            ListTile(
              title: const Text('Nested go_router route'),
              subtitle: const Text('This route has another go_router in addition to the one used with MaterialApp above.'),
              onTap: () {
                context.push('/nested_navigators');
              },
            ),
          ],
        ),
      ),
    );
  }
}

class _NestedGoRoutersPage extends StatefulWidget {
  @OverRide
  State<_NestedGoRoutersPage> createState() => _NestedGoRoutersPageState();
}

class _NestedGoRoutersPageState extends State<_NestedGoRoutersPage> {
  late final GoRouter _router;
  final GlobalKey<NavigatorState> _nestedNavigatorKey = GlobalKey<NavigatorState>();

  // If the nested navigator has routes that can be popped, then we want to
  // block the root navigator from handling the pop so that the nested navigator
  // can handle it instead.
  bool get _popEnabled {
    // canPop will throw an error if called before build. Is this the best way
    // to avoid that?
    return _nestedNavigatorKey.currentState == null ? true : !_router.canPop();
  }

  void _onRouterChanged() {
    // Here the _router reports the location correctly, but canPop is still out
    // of date.  Hence the post frame callback.
    SchedulerBinding.instance.addPostFrameCallback((Duration duration) {
      setState(() {});
    });
  }

  @OverRide
  void initState() {
    super.initState();

    final BuildContext rootContext = context;
    _router = GoRouter(
      navigatorKey: _nestedNavigatorKey,
      routes: [
        GoRoute(
          path: '/',
          builder: (BuildContext context, GoRouterState state) => _LinksPage(
            title: 'Nested once - home route',
            backgroundColor: Colors.indigo,
            onBack: () {
              rootContext.pop();
            },
            buttons: <Widget>[
              TextButton(
                onPressed: () {
                  context.push('/two');
                },
                child: const Text('Go to another route in this nested Navigator'),
              ),
            ],
          ),
        ),
        GoRoute(
          path: '/two',
          builder: (BuildContext context, GoRouterState state) => _LinksPage(
            backgroundColor: Colors.indigo.withBlue(255),
            title: 'Nested once - page two',
          ),
        ),
      ],
    );

    _router.addListener(_onRouterChanged);
  }

  @OverRide
  void dispose() {
    _router.removeListener(_onRouterChanged);
    super.dispose();
  }

  @OverRide
  Widget build(BuildContext context) {
    return PopScope(
      popEnabled: _popEnabled,
      onPopped: (bool success) {
        if (success) {
          return;
        }
        _router.pop();
      },
      child: Router<Object>.withConfig(
        restorationScopeId: 'router-2',
        config: _router,
      ),
    );
  }
}

class _LinksPage extends StatelessWidget {
  const _LinksPage ({
    required this.backgroundColor,
    this.buttons = const <Widget>[],
    this.onBack,
    required this.title,
  });

  final Color backgroundColor;
  final List<Widget> buttons;
  final VoidCallback? onBack;
  final String title;

  @OverRide
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: backgroundColor,
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Text(title),
            //const Text('A system back here will go back to Nested Navigators Page One'),
            ...buttons,
            TextButton(
              onPressed: onBack ?? () {
                context.pop();
              },
              child: const Text('Go back'),
            ),
          ],
        ),
      ),
    );
  }
}
```

</details>

### Resources

Fixes #109513
Depends on engine PR flutter/engine#39208 ✔️ 
Design doc: https://docs.google.com/document/d/1BGCWy1_LRrXEB6qeqTAKlk-U2CZlKJ5xI97g45U7azk/edit#
Migration guide: flutter/website#8952
@justinmc justinmc marked this pull request as ready for review August 4, 2023 22:16
@justinmc
Copy link
Contributor Author

justinmc commented Aug 4, 2023

The related PRs have merged so this can be merged too now if the checks pass.

@justinmc justinmc merged commit 80151fc into flutter:main Aug 7, 2023
9 checks passed
@justinmc justinmc deleted the predictive-back branch August 7, 2023 22:21
parlough added a commit that referenced this pull request Aug 21, 2023
A user requested this example be added to the migration guide [on
Discord](https://discord.com/channels/608014603317936148/969301283825659914/1140422179511603290).
I think it's a reasonable request.

Follow up on: #8952
Framework PR: flutter/flutter#132249

---------

Co-authored-by: Parker Lougheed <parlough@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
act.merge-to-next Merge PR to next stable branch release
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants