• Justin McCandless's avatar
    Predictive back support for root routes (#120385) · dedd100e
    Justin McCandless authored
    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 https://github.com/flutter/flutter/issues/109513
    Depends on engine PR https://github.com/flutter/engine/pull/39208  
    Design doc: https://docs.google.com/document/d/1BGCWy1_LRrXEB6qeqTAKlk-U2CZlKJ5xI97g45U7azk/edit#
    Migration guide: https://github.com/flutter/website/pull/8952
    dedd100e
Name
Last commit
Last update
..
_platform_selectable_region_context_menu_io.dart Loading commit data...
_platform_selectable_region_context_menu_web.dart Loading commit data...
actions.dart Loading commit data...
adapter.dart Loading commit data...
animated_cross_fade.dart Loading commit data...
animated_scroll_view.dart Loading commit data...
animated_size.dart Loading commit data...
animated_switcher.dart Loading commit data...
annotated_region.dart Loading commit data...
app.dart Loading commit data...
app_lifecycle_listener.dart Loading commit data...
async.dart Loading commit data...
autocomplete.dart Loading commit data...
autofill.dart Loading commit data...
automatic_keep_alive.dart Loading commit data...
banner.dart Loading commit data...
basic.dart Loading commit data...
binding.dart Loading commit data...
bottom_navigation_bar_item.dart Loading commit data...
color_filter.dart Loading commit data...
constants.dart Loading commit data...
container.dart Loading commit data...
context_menu_button_item.dart Loading commit data...
context_menu_controller.dart Loading commit data...
debug.dart Loading commit data...
decorated_sliver.dart Loading commit data...
default_selection_style.dart Loading commit data...
default_text_editing_shortcuts.dart Loading commit data...
desktop_text_selection_toolbar_layout_delegate.dart Loading commit data...
dismissible.dart Loading commit data...
display_feature_sub_screen.dart Loading commit data...
disposable_build_context.dart Loading commit data...
drag_target.dart Loading commit data...
draggable_scrollable_sheet.dart Loading commit data...
dual_transition_builder.dart Loading commit data...
editable_text.dart Loading commit data...
fade_in_image.dart Loading commit data...
focus_manager.dart Loading commit data...
focus_scope.dart Loading commit data...
focus_traversal.dart Loading commit data...
form.dart Loading commit data...
framework.dart Loading commit data...
gesture_detector.dart Loading commit data...
grid_paper.dart Loading commit data...
heroes.dart Loading commit data...
icon.dart Loading commit data...
icon_data.dart Loading commit data...
icon_theme.dart Loading commit data...
icon_theme_data.dart Loading commit data...
image.dart Loading commit data...
image_filter.dart Loading commit data...
image_icon.dart Loading commit data...
implicit_animations.dart Loading commit data...
inherited_model.dart Loading commit data...
inherited_notifier.dart Loading commit data...
inherited_theme.dart Loading commit data...
interactive_viewer.dart Loading commit data...
keyboard_listener.dart Loading commit data...
layout_builder.dart Loading commit data...
list_wheel_scroll_view.dart Loading commit data...
localizations.dart Loading commit data...
lookup_boundary.dart Loading commit data...
magnifier.dart Loading commit data...
media_query.dart Loading commit data...
modal_barrier.dart Loading commit data...
navigation_toolbar.dart Loading commit data...
navigator.dart Loading commit data...
navigator_pop_handler.dart Loading commit data...
nested_scroll_view.dart Loading commit data...
notification_listener.dart Loading commit data...
orientation_builder.dart Loading commit data...
overflow_bar.dart Loading commit data...
overlay.dart Loading commit data...
overscroll_indicator.dart Loading commit data...
page_storage.dart Loading commit data...
page_view.dart Loading commit data...
pages.dart Loading commit data...
performance_overlay.dart Loading commit data...
placeholder.dart Loading commit data...
platform_menu_bar.dart Loading commit data...
platform_selectable_region_context_menu.dart Loading commit data...
platform_view.dart Loading commit data...
pop_scope.dart Loading commit data...
preferred_size.dart Loading commit data...
primary_scroll_controller.dart Loading commit data...
raw_keyboard_listener.dart Loading commit data...
reorderable_list.dart Loading commit data...
restoration.dart Loading commit data...
restoration_properties.dart Loading commit data...
router.dart Loading commit data...
routes.dart Loading commit data...
safe_area.dart Loading commit data...
scroll_activity.dart Loading commit data...
scroll_aware_image_provider.dart Loading commit data...
scroll_configuration.dart Loading commit data...
scroll_context.dart Loading commit data...
scroll_controller.dart Loading commit data...
scroll_delegate.dart Loading commit data...
scroll_metrics.dart Loading commit data...
scroll_notification.dart Loading commit data...
scroll_notification_observer.dart Loading commit data...
scroll_physics.dart Loading commit data...
scroll_position.dart Loading commit data...
scroll_position_with_single_context.dart Loading commit data...
scroll_simulation.dart Loading commit data...
scroll_view.dart Loading commit data...
scrollable.dart Loading commit data...
scrollable_helpers.dart Loading commit data...
scrollbar.dart Loading commit data...
selectable_region.dart Loading commit data...
selection_container.dart Loading commit data...
semantics_debugger.dart Loading commit data...
service_extensions.dart Loading commit data...
shared_app_data.dart Loading commit data...
shortcuts.dart Loading commit data...
single_child_scroll_view.dart Loading commit data...
size_changed_layout_notifier.dart Loading commit data...
sliver.dart Loading commit data...
sliver_fill.dart Loading commit data...
sliver_layout_builder.dart Loading commit data...
sliver_persistent_header.dart Loading commit data...
sliver_prototype_extent_list.dart Loading commit data...
slotted_render_object_widget.dart Loading commit data...
snapshot_widget.dart Loading commit data...
spacer.dart Loading commit data...
spell_check.dart Loading commit data...
status_transitions.dart Loading commit data...
table.dart Loading commit data...
tap_region.dart Loading commit data...
text.dart Loading commit data...
text_editing_intents.dart Loading commit data...
text_selection.dart Loading commit data...
text_selection_toolbar_anchors.dart Loading commit data...
text_selection_toolbar_layout_delegate.dart Loading commit data...
texture.dart Loading commit data...
ticker_provider.dart Loading commit data...
title.dart Loading commit data...
transitions.dart Loading commit data...
tween_animation_builder.dart Loading commit data...
two_dimensional_scroll_view.dart Loading commit data...
two_dimensional_viewport.dart Loading commit data...
undo_history.dart Loading commit data...
unique_widget.dart Loading commit data...
value_listenable_builder.dart Loading commit data...
view.dart Loading commit data...
viewport.dart Loading commit data...
visibility.dart Loading commit data...
widget_inspector.dart Loading commit data...
widget_span.dart Loading commit data...
will_pop_scope.dart Loading commit data...
window.dart Loading commit data...