user3056783
user3056783

Reputation: 2616

Flutter: nested routes scaffold not showing back navigation

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:

Flutter application with nested navigation

Code

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

Answers (3)

BlackJerry
BlackJerry

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

user3056783
user3056783

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:

Flutter nested navigation routes with back button

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

Paweł Zawiślak
Paweł Zawiślak

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

Related Questions