'Flutter Nested Navigation Back Button handling on Android
Background
I have a Navigator widget on my InitialPage
and I am pushing two routes ontop of it (NestedFirstRoute
and NestedSecondRoute
). When I press the physical back button on Android, Both the routes in Navigator are popped (which is expected).
Use case
So I would like to handle this case when the back button is pressed only the top route (NestedSecondRoute
) must be popped.
Solution I tried
To deal with this issue I have wrapped the Navigator widget in WillPopScope to handle the back button press events and assigned keys to nested routes so as to use them when popping routes in the willPop scope.
I get an exception on this line
if (NestedFirstPage.firstPageKey.currentState!.canPop()) {
Exception has occurred.
_CastError (Null check operator used on a null value)
Heres the minimal and complete code sample
import 'package:flutter/material.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
MyApp({Key? key}) : super(key: key);
final _navigatorKey = GlobalKey<NavigatorState>();
@override
Widget build(BuildContext context) {
return MaterialApp(
onGenerateRoute: (settings) {
switch (settings.name) {
case NestedFirstPage.route:
return MaterialPageRoute(
builder: (context) {
return WillPopScope(
onWillPop: () async {
if (NestedFirstPage.firstPageKey.currentState!.canPop()) {
NestedFirstPage.firstPageKey.currentState!.pop();
return false;
} else if (NestedSecondPage.secondPageKey.currentState!
.canPop()) {
NestedSecondPage.secondPageKey.currentState!.pop();
return false;
}
return true;
},
child: Navigator(
key: _navigatorKey,
onGenerateRoute: (settings) {
switch (settings.name) {
case Navigator.defaultRouteName:
return MaterialPageRoute(
builder: (context) => const NestedFirstPage(),
settings: settings,
);
case NestedSecondPage.route:
return MaterialPageRoute(
builder: (context) => const NestedSecondPage(),
settings: settings,
);
}
},
),
);
},
settings: settings,
);
}
},
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: const InitialPage(title: 'Initial Page'),
);
}
}
class InitialPage extends StatelessWidget {
const InitialPage({Key? key, required this.title}) : super(key: key);
final String title;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
OutlinedButton(
onPressed: () {
Navigator.pushNamed(context, NestedFirstPage.route);
},
child: const Text('Move to Nested First Page'),
),
],
),
),
);
}
}
class NestedFirstPage extends StatelessWidget {
const NestedFirstPage({Key? key}) : super(key: key);
static final GlobalKey<NavigatorState> firstPageKey =
GlobalKey<NavigatorState>();
static const String route = '/nested/first';
@override
Widget build(BuildContext context) {
return Scaffold(
key: firstPageKey,
appBar: AppBar(title: const Text('Nested First Page')),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Text('First page'),
OutlinedButton(
child: const Text('Move to Nested Second Page'),
onPressed: () {
Navigator.pushNamed(context, NestedSecondPage.route);
},
),
],
),
),
);
}
}
class NestedSecondPage extends StatelessWidget {
const NestedSecondPage({Key? key}) : super(key: key);
static final GlobalKey<NavigatorState> secondPageKey =
GlobalKey<NavigatorState>();
static const String route = '/nested/second';
@override
Widget build(BuildContext context) {
return Scaffold(
key: secondPageKey,
appBar: AppBar(title: const Text('Nested Second Page')),
body: const Center(
child: Text('Second Page'),
),
);
}
}
Solution 1:[1]
Here's a slightly modified version of the above code which will allow pushing nested routes and can be popped via android back button
import 'package:flutter/material.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
MyApp({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
routes: {
'/nested/first': (context) => const NestedFirstPage(),
'/nested/first/second': (context) => const NestedSecondPage()
},
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: const RouteManager());
}
}
class InitialPage extends StatelessWidget {
const InitialPage({Key? key, required this.title}) : super(key: key);
final String title;
static const String route = '/';
@override
Widget build(BuildContext context) {
return WillPopScope(
onWillPop: () async {
if (navigatorKey.currentState != null &&
navigatorKey.currentState!.canPop()) {
navigatorKey.currentState!.pop();
return true;
}
return false;
},
child: Scaffold(
appBar: AppBar(
title: Text(title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
OutlinedButton(
onPressed: () {
navigate(context, NestedFirstPage.route);
},
child: const Text('Move to Nested First Page'),
),
],
),
),
),
);
}
}
Future<void> navigate(BuildContext context, String route,
{bool isDialog = false, bool isRootNavigator = true}) =>
Navigator.of(context, rootNavigator: isRootNavigator).pushNamed(route);
final navigatorKey = GlobalKey<NavigatorState>();
class RouteManager extends StatelessWidget {
const RouteManager({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Navigator(
key: navigatorKey,
initialRoute: '/',
onGenerateRoute: (RouteSettings settings) {
WidgetBuilder builder;
switch (settings.name) {
case '/nested/first':
builder = (BuildContext _) => const NestedFirstPage();
break;
case '/nested/first/second':
builder = (BuildContext _) => const NestedSecondPage();
break;
default:
builder =
(BuildContext _) => const InitialPage(title: 'Initial Page');
}
return MaterialPageRoute(builder: builder, settings: settings);
});
}
}
class NestedFirstPage extends StatelessWidget {
static final GlobalKey<NavigatorState> firstPageKey =
GlobalKey<NavigatorState>();
static const String route = '/nested/first';
const NestedFirstPage({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Scaffold(
key: firstPageKey,
appBar: AppBar(title: const Text('Nested First Page')),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Text('First page'),
OutlinedButton(
child: const Text('Move to Nested Second Page'),
onPressed: () {
navigate(context, NestedSecondPage.route);
},
),
],
),
),
);
}
}
class NestedSecondPage extends StatelessWidget {
const NestedSecondPage({Key? key}) : super(key: key);
static final GlobalKey<NavigatorState> secondPageKey =
GlobalKey<NavigatorState>();
static const String route = '/nested/first/second';
@override
Widget build(BuildContext context) {
return Scaffold(
key: secondPageKey,
appBar: AppBar(title: const Text('Nested Second Page')),
body: const Center(
child: Text('Second Page'),
),
);
}
}
Heres a real world example with a Nested Bottomavigationbar.
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
Solution | Source |
---|---|
Solution 1 | Mahesh Jamdade |