Edit February 2024: WillPopScope has been deprecated in favor of PopScope. The snippets have been updated accordingly but not rigorously tested. Check out the example repo for a full working example.
Many apps nowadays use a bottom navigation bar to allow the user to quickly switch between the main sections of the app. The Material design guide explains that bottom navigation generally behaves differently on Android and iOS. While on Android the app always navigates to the top-level screen of a section (resetting the state of that section), iOS reflects prior user interaction by showing the last viewed screen of the section. The design guide further states that, in order to improve the user experience, it is fine to override platform standards and go with any of the two approaches.
Let’s see how to implement an app that uses a bottom navigation bar, keeps the state of the different sections and uses nested navigation to navigate within the sections. Assume we have an app that uses a bottom navigation bar with two entries: “Book” and “Coffee”. Each entry leads to a top-level screen of the respective section titled “Books 1” and “Coffee 1”. From there we have a FlatButton that takes us to “Books 2” and “Coffee 2”. We want the sections to keep their state, so that we can conveniently switch between them.
The code for our app would look something like this:
We use an IndexedStack to preserve the state of the different sections. To allow navigation within the sections we return custom navigators. This way we can encapsulate what happens within the sections. Building the app and playing around with it, it seems that everything works fine and we can navigate inside the sections using the back button in the AppBar. That is, until we navigate to “Books 2” (or “Coffee 2”) on Android, tap the back button provided by the Android system and see that the app closes instead of navigating back to “Books 1”.
What happened here? It turns out that Android doesn’t know from which navigator to pop when the back button is tapped and as a result uses the root navigator, which then closes the app. To avoid this we have to get three things done:
- Catch the back button press
- Determine the currently active nested navigator
- Pop from the active navigator if possible or otherwise close the app
First, we wrap our scaffold into a PopScope which allows us to run a callback (specified as onPopInvoked) when our app is about to be closed. This is where not using nested scaffolds comes into play. When I used nested scaffolds, no matter where I put the PopScope the onPopInvoked callback never got triggered. Using only one Scaffold as the root widget and using Column widgets for the sections did the trick. We will talk about _systemBackButtonPressed in a second.
In order to find the currently active navigator, we have to use global keys. I will only show how to do that for the book navigator here, but every navigator gets one.
We can now use the _systemBackButtonPressed callback to find the active nested navigator and check whether it can pop something. In that case we pop from the nested navigator, otherwise we close the app. We find the active navigator be keeping a list of the navigator keys and using the index of the selected bottom navigation tab to get the active one. The state for our home page should finally look like this:
After implementing all three steps the Android system back button works as expected!
To summarize
We have learned how to use a bottom navigation bar with nested navigation in each section of the app, while retaining the state of each section. Navigating back is possible using the back button in the app bar, as well as using the Android system back button. Keep in mind not to nest scaffolds or you won’t be able to catch the back button press.
The source code for the full working example is available here.