Skip to content

Predictive back route transitions don't work with interleaved routes from multiple Navigators #152578

@justinmc

Description

@justinmc

Steps to reproduce

Imagine a routing structure with two nested Navigators, where the root Navigator can show a route on top of the nested Navigator (what I'm calling interleaving). When performing a back gesture on Android on that top route, the route underneath, in the nested Navigator, will be popped instead of the top route on the root Navigator.

  1. Run an app with interleaving, such as the one given below, on an Android device with predictive back enabled.
  2. Navigate to the final page, which is the final route of the root Navigator.
  3. Perform a back gesture.

Expected results

The visible route shows the predictive back route transition and is popped.

Actual results

Nothing happens (actually, the route underneath, which is the final route of the nested Navigator, is popped). Performing another back gesture will go back to the first route of the nested Navigator.

Code sample

Code sample
import 'package:flutter/material.dart';

void main() async {
  runApp(const _MyApp());
}

class _MyApp extends StatelessWidget {
  const _MyApp();

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      theme: ThemeData(
        useMaterial3: false,
        pageTransitionsTheme: const PageTransitionsTheme(
          builders: <TargetPlatform, PageTransitionsBuilder>{
            TargetPlatform.android: PredictiveBackPageTransitionsBuilder(),
          },
        ),
      ),
      title: 'Flutter Demo',
      routes: {
        '/': (BuildContext context) {
          return _NestedNavigatorPage();
        },
        'nav1leaf': (BuildContext context) {
          return const _Nav1LeafPage();
        },
      },
    );
  }
}

class _NestedNavigatorPage extends StatelessWidget {
  _NestedNavigatorPage();

  final GlobalKey<NavigatorState> _nestedNavigatorKey = GlobalKey<NavigatorState>();

  @override
  Widget build(BuildContext context) {
    return Navigator(
      key: _nestedNavigatorKey,
      initialRoute: 'nav2home',
      onGenerateRoute: (RouteSettings settings) {
        return switch (settings.name) {
          'nav2home' => MaterialPageRoute(
            builder: (BuildContext context) => const _Nav2HomePage(),
          ),
          'nav2leaf' => MaterialPageRoute(
            builder: (BuildContext context) => const _Nav2LeafPage(),
          ),
          _ => MaterialPageRoute(
            builder: (BuildContext context) => const Text('404'),
          ),
        };
      },
    );
  }
}

class _Nav2HomePage extends StatelessWidget {
  const _Nav2HomePage();

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: Colors.blueAccent,
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            const Text('Home page of Navigator 2'),
            const SizedBox(height: 16, width: double.infinity),
            ElevatedButton(
              child: const Text('Open nav 2 leaf page'),
              onPressed: () => Navigator.of(context).pushNamed('nav2leaf'),
            ),
          ],
        ),
      ),
    );
  }
}

class _Nav2LeafPage extends StatelessWidget {
  const _Nav2LeafPage();

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: Colors.blue,
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            const Text('Leaf page of Navigator 2'),
            const SizedBox(height: 16, width: double.infinity),
            ElevatedButton(
              child: const Text('Open nav 1 leaf page'),
              onPressed: () => Navigator.of(context, rootNavigator: true).pushNamed('nav1leaf'),
            ),
          ],
        ),
      ),
    );
  }
}

class _Nav1LeafPage extends StatelessWidget {
  const _Nav1LeafPage();

  @override
  Widget build(BuildContext context) {
    return const Scaffold(
      body: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        crossAxisAlignment: CrossAxisAlignment.center,
        children: [
          Text('Leaf of Navigator 1'),
        ],
      ),
    );
  }
}

Screenshots or Video

Video demonstration
Screencast.from.2024-07-30.13-30-36.webm

Logs

No response

Flutter Doctor output

Doctor output
[!] Flutter (Channel [user-branch], 3.22.0-35.0.pre.889, on Debian GNU/Linux rodete 6.6.15-2rodete2-amd64, locale en_US.UTF-8)
    ! Flutter version 3.22.0-35.0.pre.889 on channel [user-branch] at /usr/local/google/home/jmccandless/Projects/flutter
      Currently on an unknown channel. Run `flutter channel` to switch to an official channel.
      If that doesn't fix the issue, reinstall Flutter by following instructions at https://flutter.dev/setup.
[✓] Android toolchain - develop for Android devices (Android SDK version 34.0.0)
[✓] Chrome - develop for the web
[✓] Linux toolchain - develop for Linux desktop
[✓] Android Studio (version 2023.1)
[✓] Android Studio (version 2023.2)
[✓] Connected device (2 available)
[✓] Network resources

! Doctor found issues in 1 category.

Why does this happen?

This bug is reproduced only when using the PredictiveBackPageTransitionsBuilder and not with any other transition. The reason is that when using any other transition, the back gesture is sent via handlePopRoute and handled by WidgetsApp. WidgetsApp calls maybePop on the root Navigator (which might be caught by PopScope which then pops the nested Navigator, if needed). This all works.

However with predictive back, it's sent via startBackGesture and handled by _PredictiveBackGestureDetector. This seems to call handleStartBack on the _PredictiveBackGestureDetector for the nested Navigator, not the root one.

How can it be fixed

Maybe _PredictiveBackGestureDetector.handleStartBack needs to be smart enough to return false when it's not the topmost route? It might be hard to determine that in a nested Navigator scenario, though.

Metadata

Metadata

Assignees

No one assigned

    Labels

    r: duplicateIssue is closed as a duplicate of an existing issue

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions