Using the new Navigator 2.0 API for navigation in my Flutter app, I have the following simple version of my code:
import 'package:flutter/material.dart';
void main() {
runApp(const MainApp());
}
class MainApp extends StatelessWidget {
const MainApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Router(
routerDelegate: AppRouterDelegate(),
backButtonDispatcher: RootBackButtonDispatcher(),
),
);
}
}
class MainPage extends StatelessWidget {
const MainPage({super.key, required this.openFirstPage});
final VoidCallback openFirstPage;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Main page'),
backgroundColor: Colors.lightGreen,
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Text('Welcome to main page!'),
ElevatedButton(
onPressed: openFirstPage,
child: const Text('Go to first page'),
),
],
),
),
);
}
}
class FirstPage extends StatelessWidget {
const FirstPage({super.key, required this.openSecondPage});
final VoidCallback openSecondPage;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('First page'),
backgroundColor: Colors.amber,
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Text('Welcome to first page!'),
ElevatedButton(
onPressed: openSecondPage,
child: const Text('Go to second page'),
),
],
),
),
);
}
}
class SecondPage extends StatelessWidget {
const SecondPage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Second page'),
backgroundColor: Colors.lightBlueAccent,
),
body: const Center(
child: Text('Welcome to second page!'),
),
);
}
}
class AppRouterDelegate extends RouterDelegate
with ChangeNotifier, PopNavigatorRouterDelegateMixin {
bool _showFirstPage = false;
void showFirstPage() {
_showFirstPage = true;
notifyListeners();
}
bool _showSecondPage = false;
void showSecondPage() {
_showSecondPage = true;
notifyListeners();
}
@override
Widget build(BuildContext context) {
return Navigator(
key: navigatorKey,
pages: [
MaterialPage(
key: const ValueKey('MainPage'),
child: MainPage(openFirstPage: showFirstPage),
),
if (_showFirstPage)
MaterialPage(
key: const ValueKey('FirstPage'),
child: FirstPage(openSecondPage: showSecondPage),
),
if (_showSecondPage)
const MaterialPage(
key: ValueKey('SecondPage'),
child: SecondPage(),
),
],
onDidRemovePage: (page) {
if (_showSecondPage) {
_showSecondPage = false;
return;
}
if (_showFirstPage) _showFirstPage = false;
},
);
}
@override
GlobalKey<NavigatorState>? get navigatorKey => GlobalKey();
@override
Future<void> setNewRoutePath(configuration) async {
// Do nothing
}
}
When clicking on the back button in the AppBar
, the pages are popped as expected (the current page is closed and the one below it is shown):
Second Page ---> pop ---
| │
| ↓
First Page First Page ---> pop ---
| | │
| | ↓
Main Page Main Page Main Page
..., but when the Android back button is pressed/swiped, the whole app is closed instead of the current page.
Second Page ---> Android back btn pop ---
| │
| |
First Page |
| |
| ↓
Main Page App is closed
Here's an illustration:
After examining different parts of my code, it turned out the code for AppRouterDelegate
was wrong! Here's the correct version:
class AppRouterDelegate extends RouterDelegate<bool>
with ChangeNotifier, PopNavigatorRouterDelegateMixin {
final GlobalKey<NavigatorState> _navigatorKey = GlobalKey();
bool _showFirstPage = false;
void showFirstPage() {
_showFirstPage = true;
notifyListeners();
}
bool _showSecondPage = false;
void showSecondPage() {
_showSecondPage = true;
notifyListeners();
}
@override
Widget build(BuildContext context) {
return Navigator(
key: navigatorKey,
pages: [
MaterialPage(
key: const ValueKey('MainPage'),
child: MainPage(openFirstPage: showFirstPage),
),
if (_showFirstPage)
MaterialPage(
key: const ValueKey('FirstPage'),
child: FirstPage(openSecondPage: showSecondPage),
),
if (_showSecondPage)
const MaterialPage(
key: ValueKey('SecondPage'),
child: SecondPage(),
),
],
onDidRemovePage: (page) {
if (_showSecondPage) {
_showSecondPage = false;
return;
}
if (_showFirstPage) _showFirstPage = false;
},
);
}
@override
GlobalKey<NavigatorState>? get navigatorKey => _navigatorKey;
@override
Future<void> setNewRoutePath(configuration) async {
// Do nothing
}
}
The problem with the previous version was the following:
@override
GlobalKey<NavigatorState>? get navigatorKey => GlobalKey();
In that version, whenever navigatorKey
getter is accessed (internally by the Flutter code), it will return a new instance of GlobalKey
resulting in losing the instance attached to the Navigator
when first built.
In the current version:
@override
GlobalKey<NavigatorState>? get navigatorKey => _navigatorKey;
... navigatorKey
always points to a persistent global key, _navigatorKey
, which remains the same in all cases.