'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