Flutter nested declarative navigation

Issue

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?

Solution

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.

Answered By – tnc1997

Answer Checked By – David Goodson (FlutterFixes Volunteer)

Leave a Reply

Your email address will not be published.