Reputation: 6763
I'm trying to implement the new, declarative Navigation
style in a Flutter Material mobile app. I'd like to have multiple pages stacked on top of each other, with the ability to pop pages one-by-one via the Scaffolds back button.
Example: An app has a primary screen listing breakfast ingredients, a detail page for all ingredients, and finally a page that can be opened from the detail page, listing people who liked that particular ingredient. Focusing only on the navigation part, this is the setup:
class MainNav extends StatelessWidget {
@override
Widget build(BuildContext context) => Navigator(pages: [
MaterialPage(key: ValueKey("one"), child: AppShell(title: "Breakfast ingredients", content: "Egg, Crispy bacon, beans")),
MaterialPage(key: ValueKey("two"), child: AppShell(title: "Crispy bacon", content: "It's crispy!")),
MaterialPage(key: ValueKey("three"), child: AppShell(title: "People who like Crispy bacon", content: "John, Jill")),
], onPopPage: (route, result) => route.didPop(result));
}
See a bare minimum working version (state management and everything else left out for simplicity): https://dartpad.dev/3d3451aba0f97016ae8c4b86a8b132bb
The navigation pop works as expected: the app starts from the last page defined in the pages
list, and by clicking the Scaffold
back button, pages can be popped one by one up to the top level page. So far so good.
Now I'd like to organize widgets so that navigating to the "People who liked..." subpage is not a responsibility of the MainNav
any more, but rather the responsibility of the ingredient detail page. I introduced an intermediate Navigation
that pushes the detail page on to the pages stack, and if required, also the "People who liked..." page. Like:
class MainNav extends StatelessWidget {
@override
Widget build(BuildContext context) => Navigator(pages: [
MaterialPage(key: ValueKey("one"), child: AppShell(title: "Breakfast ingredients", content: "Egg, Crispy bacon, beans")),
MaterialPage(key: ValueKey("sub"), child: SubNav())
], onPopPage: (route, result) => route.didPop(result));
}
class SubNav extends StatelessWidget {
@override
Widget build(BuildContext context) => Navigator(pages: [
MaterialPage(key: ValueKey("two"), child: AppShell(title: "Crispy bacon", content: "It's crispy!")),
MaterialPage(key: ValueKey("three"), child: AppShell(title: "People who like Crispy bacon", content: "John, Jill"))
],
onPopPage: (route, result) => route.didPop(result));
}
See live version: https://dartpad.dev/d0c38e2ec443aafc30d9e9f82ef158d9
What happens here is after popping the last page from the list, we're on the "Crispy bacon" page, with no possibility to pop this one and go back to the list of "Breakfast ingredients" page. I'm probably missing something fundamental about navigation context, and this is not the way to implement hierarchical, declarative style navigation.
So the question: how to delegate navigation tasks down the widget hierarchy, only using declarative navigation concepts? Is there any way to create nested Navigation
widgets and have all their pages
behave as a single page hierarchy?
Upvotes: 0
Views: 404
Reputation: 1962
A minimum working example showing one of the methods that you could use to navigate from the inner SubNav
widget to the outer MainNav
widget can be found below:
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Nested declarative navigator',
debugShowCheckedModeBanner: false,
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: MainNav(),
);
}
}
class MainNav extends StatelessWidget {
@override
Widget build(BuildContext context) => Navigator(pages: [
MaterialPage(key: ValueKey("one"), child: AppShell(title: "Breakfast ingredients", content: "Egg, Crispy bacon, beans")),
MaterialPage(key: ValueKey("sub"), child: SubNav())
], onPopPage: (route, result) => route.didPop(result));
}
class SubNav extends StatelessWidget {
@override
Widget build(BuildContext context) => Navigator(pages: [
MaterialPage(key: ValueKey("two"), child: AppShell(title: "Crispy bacon", content: "It's crispy!", leading: BackButton(onPressed: () => Navigator.of(context).maybePop()))),
MaterialPage(key: ValueKey("three"), child: AppShell(title: "People who like Crispy bacon", content: "John, Jill"))
],
onPopPage: (route, result) => route.didPop(result));
}
class AppShell extends StatelessWidget {
final String title;
final String content;
final Widget leading;
const AppShell({Key key, this.title, this.content, this.leading}) : super(key: key);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(title),
leading: leading
),
body: Center(child: Text(content)));
}
}
I have added an optional leading
parameter to the constructor of the AppShell
widget. This allows us to pass in a BackButton
widget that we can create ourselves in the SubNav
widget. The BackButton
widget has an onPressed
parameter, which we can use to maybePop
the Navigator
created in the MainNav
widget. This is possible because the BuildContext
passed into the build
method of the SubNav
widget is outside of the inner Navigator
, hence calling Navigator.of(context)
inside the build
method returns the NavigatorState
of the outer Navigator
that is created in the MainNav
widget.
Upvotes: 1