Flutter Navigation with Confirmation

I spend a lot of time in the Flutter slack channel answering questions to help others. Sometimes I am asked a question that seems trivial at first, and then I realize I’ve been working with Flutter since 2017 and even I sometimes struggled with the simplest of tasks when I started. So, when I can, I try to help even those who ask even the simplest questions. The ones no one else seems to bother with. Today I got such a question, “How do I have the user confirm they want to navigate away from the current page when I’m using the bottom navigation bar?” Seems easy enough and it is if you’ve worked with Flutter for a while. However, for those newbies, I thought I would document a simple solution so I can just point to it the next time this question comes up. And, it comes up more often than you might think.

I’m going to present an example using the sample code from Flutter’s BottomNavigationBar documentation page which you can find here: https://api.flutter.dev/flutter/material/BottomNavigationBar-class.html . The sample app presents four pages that can be navigated to by clicking on an icon in the bottom navigation bar. Let’s see the code.

import 'package:flutter/material.dart';

void main() {
  runApp(MaterialApp(home: MyApp()));
}

class MyApp extends StatefulWidget {
  @override
  _MyAppState createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {
  bool _confirmNavigation;

  int _selectedIndex = 0;
  static const TextStyle optionStyle =
      TextStyle(fontSize: 30, fontWeight: FontWeight.bold);
  static const List<Widget> _widgetOptions = <Widget>[
    Text(
      'Index 0: Home',
      style: optionStyle,
    ),
    Text(
      'Index 1: Business',
      style: optionStyle,
    ),
    Text(
      'Index 2: School',
      style: optionStyle,
    ),
    Text(
      'Index 3: Settings',
      style: optionStyle,
    ),
  ];

  var confirmedNavPages = <int>[1, 2];

  @override
  void initState() {
    super.initState();
    _confirmNavigation = false;
  }

  _showNavConfirmDialog(int index) async {
    return showDialog(
      context: context,
      builder: (BuildContext context) {
        return AlertDialog(
          title: Text('Leave Page'),
          content: Text('Do you really want to leave this page?'),
          actions: [
            TextButton(
              child: Text('Yes'),
              onPressed: () {
                setState(() {
                  _confirmNavigation = true;
                });
                _selectedIndex = index;
                Navigator.of(context).pop();
              },
            ),
            TextButton(
              child: Text('No'),
              onPressed: () {
                print('Page Not confirmed');
                setState(() {
                  _confirmNavigation = false;
                });
                Navigator.of(context).pop();
              },
            ),
          ],
        );
      },
    );
  }

  void _onItemTapped(int index) {
    if (confirmedNavPages.contains(_selectedIndex)) {
      print('Page $index needs confirmed');
      _showNavConfirmDialog(index);
    } else {
      setState(() {
        _selectedIndex = index;
      });
    }
  }

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Confirm Nav Demo',
      home: Scaffold(
        appBar: AppBar(
          title: const Text('Confirm Nav Demo'),
        ),
        body: Center(
          child: _widgetOptions.elementAt(_selectedIndex),
        ),
        bottomNavigationBar: BottomNavigationBar(
          items: const <BottomNavigationBarItem>[
            BottomNavigationBarItem(
              icon: Icon(Icons.home),
              label: 'Home',
              backgroundColor: Colors.red,
            ),
            BottomNavigationBarItem(
              icon: Icon(Icons.business),
              label: 'Business',
              backgroundColor: Colors.green,
            ),
            BottomNavigationBarItem(
              icon: Icon(Icons.school),
              label: 'School',
              backgroundColor: Colors.purple,
            ),
            BottomNavigationBarItem(
              icon: Icon(Icons.settings),
              label: 'Settings',
              backgroundColor: Colors.pink,
            ),
          ],
          currentIndex: _selectedIndex,
          selectedItemColor: Colors.amber[800],
          onTap: _onItemTapped,
        ),
      ),
    );
  }
}

Here we see the sample code uses a list of Text widgets to simulate pages. The _widgetOtions member (line: 18-35) stores these text widget so we can easily grab the associated widget by passing the corresponding index. We also keep the currently selected index in the _selectedIndex member. Additionally, we use a boolean _confirmNavigation to track if the user chose to continue navigating away from the current page. Typically, not all pages will need a confirmation. So to keep track of the pages that need to be confirmed, we use another member variable, confirmNavPages. This is just a list of page indexes that should respond with the confirmation dialog when the user attempts to navigate away from the page (line: 37). We must use initState to ensure the _confirmNavigation member is set to false on startup (lines: 39-43).

On lines 45 through 76, we define the alert dialog. In the actions list, we have two text buttons. In the onPressed handler of the ‘Yes’ button, we call setState to update the value of _confirmNavigation to true and set the _selectedIndex to the bottom navigation bar item index just selected by the user. Then we pop the dialog off the navigation stack returning to the page which will now be updated to the new _selectedIndex value (lines: 53-63).

In (lines: 64-72) the onPressed handler for the ‘No” button we simply set the value of _confirmNavigation to false inside setState and then pop the dialog off the navigation stack. This returns us to the current page.

The last piece of the puzzle is lines 79-87. The _onItemTapped handler for the bottom navigation bar. Here we need to first check if the current _selectedIndex value is in the list of confirmNavPages. If it is, we need to show the dialog passing the index of the destination page to _showConfirmDialog. Remember, here we want to know if the page we are currently on is a page that should show the dialog, not the page the user tried to navigate to. So, we use the _selectedIndex and not the bottom navigation bar’s item index. However, it is the destination page we may need to navigate to if the user confirms so we pass the item index into the _showConfirmDialog method for later assignment to the _selectedIndex when the user confirms they wish to navigate away from the current page. This same technique can be used for navigating to other pages or for other navigation widgets.

I hope this will help those looking for such an answer. Happy coding!

Leave a Reply

Your email address will not be published. Required fields are marked *