flutter / flutter/flutter

Interleaved nested Navigators and Predictive Back

Open
#152,323 15 comments 8 reactions 0 assignees View on GitHub
f: routes found in release: 3.22 found in release: 3.24 framework has reproducible steps P2 platform-android team-framework triaged-framework
Dominant language
Dart
Stars
179k
Forks
31.1k
PR merge metrics
PR metrics pending

Description

### Steps to reproduce

PredictiveBackPageTransitionsBuilder seems to break a certain case of using nested Navigators.

1. Run the app given below on an Android device with [predictive back enabled](https://github.com/flutter/flutter/issues/109513#issuecomment-1666220080).
2. The home page is inside of a nested Navigator. So the root Navigator and nested Navigator both have 1 route each.
3. Tap to push a route to the nested Navigator. The root Navigator has 1 route and nested Navigator has 2.
4. Tap to push a route to the root Navigator. The root Navigator and nested Navigator both have 2 routes.
5. Perform a system back gesture.

### Expected results

The current route (the root Navigator's second of 2 routes) is popped.

### Actual results

Nothing appears to happen.

Really, the nested Navigator's second of 2 routes is popped, but the user can't see that. Performing another system back gesture pops the root Navigator's second route and the user sees the nested Navigator's first and only route.

Using a different PageTransitionsBuilder works fine.

### Code sample

Code sample

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

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

class _MyApp extends StatelessWidget {
const _MyApp();

// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MaterialApp(
theme: ThemeData(
useMaterial3: false,
pageTransitionsTheme: const PageTransitionsTheme(
builders: {
// If you use a different transition, there is no bug.
TargetPlatform.android: PredictiveBackPageTransitionsBuilder(),
},
),
),
title: 'Flutter Demo',
routes: {
'/': (BuildContext context) {
return _MyNestedNavigator();
},
'nav1leaf': (BuildContext context) {
return const Nav1LeafPage();
},
},
);
}
}

class _MyNestedNavigator extends StatelessWidget {
_MyNestedNavigator();

final GlobalKey _nestedNavigatorKey = GlobalKey();

@override
Widget build(BuildContext context) {
//return Navigator(
return NavigatorPopHandler(
onPop: () {
_nestedNavigatorKey.currentState!.maybePop();
},
child: Navigator(
key: _nestedNavigatorKey,
initialRoute: 'nav2home',
onGenerateRoute: (RouteSettings settings) {
return switch (settings.name) {
'nav2home' => MaterialPageRoute(
builder: (BuildContext context) => const MyHomePage(),
),
'nav2leaf' => MaterialPageRoute(
builder: (BuildContext context) => const Nav2LeafPage(),
),
_ => MaterialPageRoute(
builder: (BuildContext context) => const Text('404'),
),
};
},
),
);
}
}

class MyHomePage extends StatelessWidget {
const MyHomePage({super.key});

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

class Nav2LeafPage extends StatelessWidget {
const Nav2LeafPage({super.key});

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

class Nav1LeafPage extends StatelessWidget {
const Nav1LeafPage({super.key});

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

```

Bonus code sample! From 152578

```dart
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.android: PredictiveBackPageTransitionsBuilder(),
},
),
),
title: 'Flutter Demo',
routes: {
'/': (BuildContext context) {
return _NestedNavigatorPage();
},
'nav1leaf': (BuildContext context) {
return const _Nav1LeafPage();
},
},
);
}
}

class _NestedNavigatorPage extends StatelessWidget {
_NestedNavigatorPage();

final GlobalKey _nestedNavigatorKey = GlobalKey();

@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: [
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: [
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](https://github.com/user-attachments/assets/759d3f89-f5fd-4120-ac96-1f8477a479c6)

### Logs

_No response_

### Flutter Doctor output

Doctor output

```console
Doctor summary (to see all details, run flutter doctor -v):
[✓] Flutter (Channel master, 3.22.0-35.0.pre.918, on Debian GNU/Linux rodete 6.6.15-2rodete2-amd64, locale en_US.UTF-8)
[✓] 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 (3 available)
[✓] Network resources

• No issues found!

```

### 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](https://github.com/flutter/flutter/blob/dc6a15995e3c4f17316533045e2233b18941d82c/packages/flutter/lib/src/material/predictive_back_page_transitions_builder.dart#L122) 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.

Duplicate: https://github.com/flutter/flutter/issues/152578

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.