Reputation: 2616
I want to leverage nested routes in Flutter. I think I managed what I wanted apart from one thing. My nested "home" route does not show back navigation in AppBar
, however hardware button will get me to previous route. Perhaps I'm not using nested routes correctly.
I have a root MaterialApp
that defines main root pages via routes
property. One of root pages is MyChildPage
that renders another Navigator
and uses onGenerateRoute
property to point user to its child pages. Navigation works but when I land on MyChildPage
(it has title "Child page"), the back button is missing:
You can run interactive example on dartpad.dev where the code is also visible.
I have everything in single file:
import 'package:flutter/material.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Nested tutorial',
theme: ThemeData(
primarySwatch: Colors.orange,
),
initialRoute: '/',
routes: {
'/': (_) => MyHomePage(),
'/sub_page': (_) => MySubPage(),
'/child': (_) => MyChildPage(),
});
}
}
class MyHomePage extends StatefulWidget {
MyHomePage({Key? key}) : super(key: key);
@override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Home'),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text('Hello this is home page'),
ElevatedButton(
child: Text('Visit sub page'),
onPressed: () {
Navigator.pushNamed(context, '/sub_page');
}),
OutlinedButton(
child: Text('Visit child page'),
onPressed: () {
Navigator.pushNamed(context, '/child');
}),
],
),
),
);
}
}
class MySubPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Home / Sub Page')),
body: Center(child: Text('Hello this is sub page')),
);
}
}
class MyChildPage extends StatefulWidget {
@override
_MyChildPageState createState() => _MyChildPageState();
}
class _MyChildPageState extends State<MyChildPage> {
@override
Widget build(BuildContext context) {
return Navigator(
initialRoute: '/',
onGenerateRoute: (settings) {
switch (settings.name) {
case '/child_1':
return MaterialPageRoute(builder: (_) => MyChildPage1());
case '/child_2':
return MaterialPageRoute(builder: (_) => MyChildPage2());
case '/':
default:
return MaterialPageRoute(builder: (_) => MyChildPageRoot());
}
});
}
}
class MyChildPageRoot extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Child page')),
body: Container(
child: Column(mainAxisAlignment: MainAxisAlignment.center, children: [
Center(child: Text('This is child page root ')),
Center(
child: OutlinedButton(
child: Text('Visit sub child page 1'),
onPressed: () {
Navigator.pushNamed(context, '/child_1');
})),
Center(
child: OutlinedButton(
child: Text('Visit sub child page 2'),
onPressed: () {
Navigator.pushNamed(context, '/child_2');
})),
])),
);
}
}
class MyChildPage1 extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Child sub page 1')),
body: Center(child: Text('You are on child sub page #1')),
);
}
}
class MyChildPage2 extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Child sub page 2')),
body: Center(child: Text('You are on child sub page #2')),
);
}
}
I am looking for a solution where I can still use named routes and I want to keep structure of multiple Navigators. I do not want to define all routes in my top-level MaterialApp
widget.
Upvotes: 0
Views: 3847
Reputation: 88
You cannot use the nested navigator to get out of the MyChildPage, because the nested navigator was only created in the MyChildPage.
You can pass the parent Navigator into the MyChildPage like so:
class MyChildPage extends StatefulWidget {
const MyChildPage({super.key, required this.parentNavigator, ...});
final NavigatorState parentNavigator;
Then, add a leading back button in the AppBar of the MyChildPage as follows:
appBar: AppBar(
...
leading: BackButton(
onPressed: () {
widget.parentNavigator.pop();
},
),
...
),
When the back button in the MyChildPage app bar is pressed, the parent navigator will be called to pop out the MyChildPage, leading you back to the previous page before you get into the MyChildPage.
Upvotes: 0
Reputation: 2616
So after some researching online, I think I've come with some solutions.
First problem of the back arrow not showing up in AppBar
was caused by defining separate Scaffold
for each of the routes (MyChildPageRoot
, MyChildPage1
and MyChildPage2
). Creating single scaffold in _MyChildPageState
and rendering Navigator
inside its body
fixed the problem.
While this might not be the most flexible solution (there can be reasons why you want different scaffold per route), I haven't figured out a different way.
After fixing this, the back navigation worked but the child routes would go back to the application route (the initial screen) instead of returning to MyChildPageRoot
.
I managed to fix this by wrapping the Scaffold
in _MyChildPageState
with WillPopScope
widget and creating separate navigator key for nested Navigator:
class _MyChildPageState extends State<MyChildPage> {
GlobalKey<NavigatorState> _navigatorKey = GlobalKey();
// <-- snip -->
@override
Widget build(BuildContext context) {
return WillPopScope(
onWillPop: () async {
final shouldPop = await _navigatorKey.currentState?.maybePop();
return shouldPop == null ? true : !shouldPop;
},
child: Scaffold(
appBar: AppBar(title: Text(_title)),
// <-- snip -->
This now results in correct back button behaviour. What I don't like about this solution of having just a top-level Scaffold
is that I have to programatically / imperatively change AppBar
title as the routes are being visited but maybe that is just an edge case for a lot of people.
This is the result:
Link to dartpad.dev for working example.
The updated code:
import 'package:flutter/material.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Nested tutorial',
theme: ThemeData(
primarySwatch: Colors.orange,
),
initialRoute: '/',
routes: {
'/': (_) => MyHomePage(),
'/sub_page': (_) => MySubPage(),
'/child': (_) => MyChildPage(),
});
}
}
class MyHomePage extends StatefulWidget {
MyHomePage({Key? key}) : super(key: key);
@override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Home'),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text('Hello this is home page'),
ElevatedButton(
child: Text('Visit sub page'),
onPressed: () {
Navigator.pushNamed(context, '/sub_page');
}),
OutlinedButton(
child: Text('Visit child page'),
onPressed: () {
Navigator.pushNamed(context, '/child');
}),
],
),
),
);
}
}
class MySubPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Home / Sub Page')),
body: Center(child: Text('Hello this is sub page')),
);
}
}
class MyChildPage extends StatefulWidget {
@override
_MyChildPageState createState() => _MyChildPageState();
}
class _MyChildPageState extends State<MyChildPage> {
GlobalKey<NavigatorState> _navigatorKey = GlobalKey();
String _title = 'Child page';
void _setTitle(String title) {
setState(() {
_title = title;
});
}
@override
Widget build(BuildContext context) {
return WillPopScope(
onWillPop: () async {
_setTitle('Child page');
final shouldPop = await _navigatorKey.currentState?.maybePop();
return shouldPop == null ? true : !shouldPop;
},
child: Scaffold(
appBar: AppBar(title: Text(_title)),
body: Navigator(
key: _navigatorKey,
initialRoute: '/',
onGenerateRoute: (settings) {
switch (settings.name) {
case '/child_1':
return MaterialPageRoute(
builder: (_) => MyChildPage1(setTitle: _setTitle));
case '/child_2':
return MaterialPageRoute(
builder: (_) => MyChildPage2(setTitle: _setTitle));
case '/':
default:
return MaterialPageRoute(
builder: (_) => MyChildPageRoot());
}
})));
}
}
class MyChildPageRoot extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Container(
child: Column(mainAxisAlignment: MainAxisAlignment.center, children: [
Center(child: Text('This is child page root ')),
Center(
child: OutlinedButton(
child: Text('Visit sub child page 1'),
onPressed: () {
Navigator.pushNamed(context, '/child_1');
})),
Center(
child: OutlinedButton(
child: Text('Visit sub child page 2'),
onPressed: () {
Navigator.pushNamed(context, '/child_2');
})),
]),
);
}
}
class MyChildPage1 extends StatefulWidget {
MyChildPage1({required this.setTitle});
final Function setTitle;
@override
_MyChildPage1State createState() => _MyChildPage1State();
}
class _MyChildPage1State extends State<MyChildPage1> {
@override
void initState() {
super.initState();
WidgetsBinding.instance!.addPostFrameCallback((_) {
widget.setTitle('Child page / 1');
});
}
@override
Widget build(BuildContext context) {
return Container(
child: Center(child: Text('You are on child sub page #1')),
);
}
}
class MyChildPage2 extends StatefulWidget {
MyChildPage2({required this.setTitle});
final Function setTitle;
@override
_MyChildPage2State createState() => _MyChildPage2State();
}
class _MyChildPage2State extends State<MyChildPage2> {
@override
void initState() {
super.initState();
WidgetsBinding.instance!.addPostFrameCallback((_) {
widget.setTitle('Child page / 2');
});
}
@override
Widget build(BuildContext context) {
return Container(
child: Center(child: Text('You are on child sub page #2')),
);
}
}
Upvotes: 2
Reputation: 157
Problem is with inital route. You created both classes with inital route:
class MyApp extends StatelessWidget {
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Nested tutorial',
theme: ThemeData(
primarySwatch: Colors.orange,
),
initialRoute: '/',
routes: {
'/': (_) => MyHomePage(),
'/sub_page': (_) => MySubPage(),
'/child': (_) => MyChildPage(),
});
}
}
And
class MyChildPage extends StatefulWidget {
@override
_MyChildPageState createState() => _MyChildPageState();
}
class _MyChildPageState extends State<MyChildPage> {
@override
Widget build(BuildContext context) {
return Navigator(
initialRoute: '/',
onGenerateRoute: (settings) {
switch (settings.name) {
case '/child_1':
return MaterialPageRoute(builder: (_) => MyChildPage1());
case '/child_2':
return MaterialPageRoute(builder: (_) => MyChildPage2());
case '/':
default:
return MaterialPageRoute(builder: (_) => MyChildPageRoot());
}
});
}
}
When You go to MyChildPage the inital route was changed and You haven't back arrow.
The solution is have all routes in one class. In below example I don't use MyChildPage. I use directly MyChildPageRoot in routes.
class MyApp extends StatelessWidget {
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Nested tutorial',
theme: ThemeData(
primarySwatch: Colors.orange,
),
initialRoute: '/',
routes: {
'/': (_) => MyHomePage(),
'/sub_page': (_) => MySubPage(),
'/child': (_) => MyChildPageRoot(),
'/child_1': (_) => MyChildPage1(),
'/child_2':(_) => MyChildPage2(),
});
}
}
Upvotes: 1