flutterflutter-navigationflutter-routes

Android back button closes the app instead of popping the current route


Using the new Navigator 2.0 API for navigation in my Flutter app, I have the following simple version of my code:

import 'package:flutter/material.dart';

void main() {
  runApp(const MainApp());
}

class MainApp extends StatelessWidget {
  const MainApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Router(
        routerDelegate: AppRouterDelegate(),
        backButtonDispatcher: RootBackButtonDispatcher(),
      ),
    );
  }
}

class MainPage extends StatelessWidget {
  const MainPage({super.key, required this.openFirstPage});

  final VoidCallback openFirstPage;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Main page'),
        backgroundColor: Colors.lightGreen,
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            const Text('Welcome to main page!'),
            ElevatedButton(
              onPressed: openFirstPage,
              child: const Text('Go to first page'),
            ),
          ],
        ),
      ),
    );
  }
}

class FirstPage extends StatelessWidget {
  const FirstPage({super.key, required this.openSecondPage});

  final VoidCallback openSecondPage;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('First page'),
        backgroundColor: Colors.amber,
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            const Text('Welcome to first page!'),
            ElevatedButton(
              onPressed: openSecondPage,
              child: const Text('Go to second page'),
            ),
          ],
        ),
      ),
    );
  }
}

class SecondPage extends StatelessWidget {
  const SecondPage({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Second page'),
        backgroundColor: Colors.lightBlueAccent,
      ),
      body: const Center(
        child: Text('Welcome to second page!'),
      ),
    );
  }
}

class AppRouterDelegate extends RouterDelegate
    with ChangeNotifier, PopNavigatorRouterDelegateMixin {
  bool _showFirstPage = false;
  void showFirstPage() {
    _showFirstPage = true;
    notifyListeners();
  }

  bool _showSecondPage = false;
  void showSecondPage() {
    _showSecondPage = true;
    notifyListeners();
  }

  @override
  Widget build(BuildContext context) {
    return Navigator(
      key: navigatorKey,
      pages: [
        MaterialPage(
          key: const ValueKey('MainPage'),
          child: MainPage(openFirstPage: showFirstPage),
        ),
        if (_showFirstPage)
          MaterialPage(
            key: const ValueKey('FirstPage'),
            child: FirstPage(openSecondPage: showSecondPage),
          ),
        if (_showSecondPage)
          const MaterialPage(
            key: ValueKey('SecondPage'),
            child: SecondPage(),
          ),
      ],
      onDidRemovePage: (page) {
        if (_showSecondPage) {
          _showSecondPage = false;
          return;
        }
        if (_showFirstPage) _showFirstPage = false;
      },
    );
  }

  @override
  GlobalKey<NavigatorState>? get navigatorKey => GlobalKey();

  @override
  Future<void> setNewRoutePath(configuration) async {
    // Do nothing
  }
}

When clicking on the back button in the AppBar, the pages are popped as expected (the current page is closed and the one below it is shown):

Second Page ---> pop ---
    |                  │
    |                  ↓
First Page        First Page ---> pop ---
    |                  |                │
    |                  |                ↓
Main Page          Main Page        Main Page

..., but when the Android back button is pressed/swiped, the whole app is closed instead of the current page.

Second Page ---> Android back btn pop ---
    |                                   │
    |                                   |
First Page                              |
    |                                   |
    |                                   ↓
Main Page                         App is closed

Here's an illustration:


Solution

  • After examining different parts of my code, it turned out the code for AppRouterDelegate was wrong! Here's the correct version:

    class AppRouterDelegate extends RouterDelegate<bool>
        with ChangeNotifier, PopNavigatorRouterDelegateMixin {
      final GlobalKey<NavigatorState> _navigatorKey = GlobalKey();
    
      bool _showFirstPage = false;
      void showFirstPage() {
        _showFirstPage = true;
        notifyListeners();
      }
    
      bool _showSecondPage = false;
      void showSecondPage() {
        _showSecondPage = true;
        notifyListeners();
      }
    
      @override
      Widget build(BuildContext context) {
        return Navigator(
          key: navigatorKey,
          pages: [
            MaterialPage(
              key: const ValueKey('MainPage'),
              child: MainPage(openFirstPage: showFirstPage),
            ),
            if (_showFirstPage)
              MaterialPage(
                key: const ValueKey('FirstPage'),
                child: FirstPage(openSecondPage: showSecondPage),
              ),
            if (_showSecondPage)
              const MaterialPage(
                key: ValueKey('SecondPage'),
                child: SecondPage(),
              ),
          ],
          onDidRemovePage: (page) {
            if (_showSecondPage) {
              _showSecondPage = false;
              return;
            }
            if (_showFirstPage) _showFirstPage = false;
          },
        );
      }
    
      @override
      GlobalKey<NavigatorState>? get navigatorKey => _navigatorKey;
    
      @override
      Future<void> setNewRoutePath(configuration) async {
        // Do nothing
      }
    }
    

    The problem with the previous version was the following:

    @override
    GlobalKey<NavigatorState>? get navigatorKey => GlobalKey();
    

    In that version, whenever navigatorKey getter is accessed (internally by the Flutter code), it will return a new instance of GlobalKey resulting in losing the instance attached to the Navigator when first built.

    In the current version:

    @override
    GlobalKey<NavigatorState>? get navigatorKey => _navigatorKey;
    

    ... navigatorKey always points to a persistent global key, _navigatorKey, which remains the same in all cases.