In this piece we’re going to tackle the popular Bottom Navigation approach. The issues brought by previous implementations of Fragments and Jetpack Navigation Component were recently fixed in the Fragments library version 1.4.0 and Navigation Component version 2.4.0-rc01.
From the humble beginnings of just using activities
To writing fragment transactions:
And to the latest version of Jetpack’s Navigation Component
Screen transitions have always been a big part of Android development. How we navigate between screens in a mobile app has changed a lot, and each new approach brought us more control but also had its own issues that developers had to deal with.
About the Jetpack Navigation Component
Since it was first introduced in March 2019, the Jetpack Navigation component has become quite popular among Android developers because of its ease of use and plethora of features. It still had its issues, one of them being multiple back-stack navigations within BottomNavigation tabs.
Unfortunately, the Jetpack Navigation component didn’t work as expected when it was released, and it took over 2 years to get to an alpha version of the fixes necessary for this to work as intended.
The issue tracker posts are big, and the changes that were made by the Android team are significant and solve a lot of problems. If you are interested in a history of how the multiple back stacks issue got to be fixed and what changes were made in the Fragments implementation, I suggest the following articles:
- Navigation Component Issue#80029773.
- Navigation: Multiple back stacks by Murat Yener
- Multiple back stacks by Ian Lake
- Fragments: rebuilding the internals by Ian Lake
The issue at its core
In order to understand the issue, we need first to know what a back-stack is. You can look at it as your web browsing history. The back-stack is what allows you to revisit the pages you’ve been through, in reverse order, by pressing the back button. Practically it is based on the First In, Last out principle of a stack: the first page you visit will be the last one you return to by pressing the back button.
A straightforward example would be viewing posts in a feed:
Navigating back will “pop” the stack, meaning it returns you to the News Article by removing the “Author’s Profile” screen from the top of the stack.
One more time and we are back to the News Feed.
And one last press of the button to completely clear the back-stack and close the application.
As mentioned before, the actual issue was related to multiple back stacks. First, let’s understand what it means.
In our application we’ll have the primary flow with its back-stack:
The Main screen is special, it contains a BottomNavigation component that can take us to three different parts of the application: Home, Travel and Profile. From any of them, we can potentially navigate further, i.e. by selecting a news article in Home, we go to the article’s details page.
Each one should have its own back-stack, and when switching between them, the state they were left in should be persisted. For example, if on the Travel page we scroll through 10 items, and then we go to Profile when we come back to Travel we should be in the same scroll position.
And so, we arrive at the issue we are tackling in this article, which is solved with the most recent library versions. The state wasn’t persisted, users would be taken to the main entry point of the flows, scrolls would be reset and, in some cases, API calls would be performed even if not necessary.
The application navigation flow
The application’s flows are pretty simple but very common:
- When the user opens the app, they are shown the Splash screen where they are redirected either to LogIn or Home, depending on their authentication status
- From the LogIn screen they are navigated to Home
- They can either navigate using BottomNavBar in the main graph or within the inner graph (i.e. home -> details).
- In the Profile screen they have the option to logout and be redirected to Splash screen which will decide where they are taken to.
You can find the whole project here:
From build.gradle (app) file
- We are using the InternalStorageManager class to handle login status.
- In order to make this as close as possible to a real project, we’ll be using koin to inject InternalStorageManager where necessary.
- Shoutout to Zhuiden for the cool viewBinding delegate we are using for activity/fragment viewBinding.
For our application’s navigation to work, we’ll be working with three core components:
- A single activity which handles navigation. The parent component which allows users to interact with our app.
- MainActivity will contain the navigation host and the application’s BottomNavigationView
- Having the BottomNavigationView defined in the activity’s layout will allow us to show it depending on the current navigation destination.
- A navigation host, the UI component used to hold our Fragments:
- We’re using FragmentContainerView, which was specifically created to handle FragmentTransactions and coordinate Fragment behavior
- Navigation graphs
- These are representations of the flows in our application. In a graph file, we determine which screens are in a flow and which screens are connected.In this context, you can view a “flow” as a feature within the app. For example the “Home” feature within the demo application is described in home_graph, which only contains HomeFragment & DetailsFragment, the only necessary Fragments for this feature
- splash_graph.xml: SplashFragment, main_graph, auth_graph
- auth_graph.xml: LoginFragment
- home_graph.xml: HomeFragment & DetailsFragment
- travel_graph: TravelFragment
- profile_graph: ProfileFragment
- main_graph: home_graph, news_graph, profile_graph
Here, each graph is linked to a BottomNavigation button. And, each graph will have its respective back-stack, as described earlier.
For our BottomNavigationView we’ll need a menu file. The items within it must have the same id value as the navigation graph they represent. This allows navigationComponent to link and relate them.
In MainActivity’s onCreate we setup the navigation controller like so:
Using the destination change listener we can determine when the BottomNavigationView is visible. Here, we do it by checking if the current destination’s id is in the list of fragment IDs which should have BottomNavigationView as visible.
NavigateUp fix for final backStackEntry
After everything was set up and done, I noticed an issue. Whenever I reach the final entry within the current navigation backStack using findNavController().navigateUp(), I’d be stuck in the same screen.
* to close the application I need to use the hardware back button
Looking into this, I noticed that the previousBackStackEntry for the navController was null and such findNavController().navigateUp() would return false. Meaning there was nothing to pop or close. This led me to implement the following Fragment extension function, which handles this case by calling the activity’s onBackPressed method.
A breath of fresh air
After having to deal with NavigationComponent’s BottomNavigation issues within multiple projects, I was very happy to see how easy it’s been to integrate the official solution. We can finally rely on the library to do most of the handling so we can focus on application-specific work, instead of fighting the library and its issues.
You can find the whole project here: