Navigation: Conditional Navigation

Murat Yener
Android Developers
Published in
4 min readApr 26, 2021

--

This is the second article in of this Navigation series. If you prefer to watch this content instead of reading, check out the video below:

Conditional Navigation

Intro

In the previous article, I used NavigationUI, implemented bottom navigation in the app and also added a SelectionFragment to enable or disable coffee tracking. However, whether we disable or enable the coffee tracker, users can still navigate to the CoffeeList fragment which doesn’t seem quite right.

In this article I’ll fix that by adding conditional navigation and directing our users to make a selection when they launch the app for the first time. I’ll use the Datastore API to persist the user’s selection, and use this to decide showing the coffeeList destination in the bottom navigation.

Preparing the app for Conditional Navigation

Here is a quick review of changes I made since last article.

If you want to review the changes you can check out the repo here. You can also check out the code from this repo if you want to follow along!

The app now can be in 3 different states.

  • DONUT_ONLY, which means the user has disabled the coffee tracking functionality
  • DONUT_AND_COFFEE, which means the user wants to track both Donut and Coffee consumption
  • NOT_SELECTED which means the user hasn’t made a selection yet and is possibly running the app for the first time or maybe they just have a hard time making up their mind 🤷

Implementing Conditional Navigation

I’ll start implementing conditional navigation within the SelectionFragment. First I get an instance of selectionViewModel so I get access to datastore. Next, I observe the user’s selection and use it to restore the status of the checkbox. To persist the user’s selection, I’ll update the state when the checkbox is clicked by calling saveCoffeeTrackerSelection().

val selectionViewModel: SelectionViewModel by viewModels {
SelectionViewModelFactory(
UserPreferencesRepository.getInstance(requireContext())
)
}
selectionViewModel.checkCoffeeTrackerEnabled().observe(
viewLifecycleOwner
) { selection ->
if (selection == UserPrefRepository.Selection.DONUT_AND_COFFEE){
binding.checkBox.isChecked = true
}
}
binding.button.setOnClickListener { button ->
val coffeeSelected = binding.checkBox.isChecked
selectionViewModel.saveCoffeeTrackerSelection(coffeeSelected)

//...

Now it is time to update the bottom tabs with the user’s selection. If the user chooses to disable coffee tracking, the only option left in the bottom tabs will be the donutList which means we can safely remove the bottom tabs. In MainActivity, I add an observer and update the visibility of the bottom tabs. To do this, I’ll add an observer and update the visibility of the BottomNavigation according to user selection.

private fun setupMenu(
selection: UserPreferencesRepository.Selection
) {
val bottomNav = findViewById<BottomNavigationView>(R.id.bottom_nav_view)
bottomNav.isVisible = when (selection) {
UserPreferencesRepository.Selection.DONUT_AND_COFFEE -> true
else -> false
}
}

In onCreate():

val selectionViewModel: SelectionViewModel by viewModels {
SelectionViewModelFactory(
UserPreferencesRepository.getInstance(this)
)
}
selectionViewModel.checkCoffeeTrackerEnabled().observe(this) { s ->
setupMenu(s)
}

Running the app in this state, you’ll see that enabling or disabling the coffee tracker adds or removes the bottom tabs from the app. This is great but wouldn’t it be nice if we automatically sent the user to make a selection when they ran the app for the first time.

DonutList is the default fragment and our start destination which means the app always starts with the DonutList, I check if the user previously made a selection and trigger navigation to SelectionFragment if they haven’t.

donutListViewModel.isFirstRun().observe(viewLifecycleOwner) { s ->
if (s == UserPreferencesRepository.Selection.NOT_SELECTED) {
val navController = findNavController()
navController.navigate(
DonutListDirections.actionDonutListToSelectionFragment()
)
}
}

Before I test this, I uninstall the app from the device so that I am sure there are no saved preferences from my previous runs. Now when I run the app, it takes me to the selectionFragment. Later launches of the app will remember the selection I made and navigate me to the correct start destination.

And that’s it! We added conditional navigation to the donut tracker app. But how can we test this flow? Deleting the app or the app data each time before running the test is not ideal. This is where testing comes to rescue!

Testing navigation

I create a new test called OneTimeFlowTest in androidTestFolder. Then I create a test called testFirstRun(), and annotate it with @Test. Now I can start implementing the test. First I create a TestNavHostController() by using the applicationContext. I also set the nav_graph from the app to the testNavigationController instance I just created.

@Test
fun testFirstRun() {
// Create a mock NavController
val mockNavController = TestNavHostController(
ApplicationProvider.getApplicationContext()
)
mockNavController.setGraph(R.navigation.nav_graph) //...
}

The mockNavigationController is ready to use. It’s time to create the scenario. To do that, I start the app with the DonutList fragment and set the mockNavigationController instance I created before. Then check if the app automatically navigates to the selectionFragment as expected.

val scenario = launchFragmentInContainer {
DonutList().also { fragment ->
fragment.viewLifecycleOwnerLiveData.observeForever{
viewLifecycleOwner ->
if (viewLifecycleOwner != null){
Navigation.setViewNavController(
fragment.requireView(),
mockNavController
)
}
}
}
}
scenario.onFragment {
assertThat(
mockNavController.currentDestination?.id
).isEqualTo(R.id.selectionFragment)
}

Now I run the test and wait for the result… and the test passes with flying colors! or well may be just green.

Testing Navigation

Summary

In this article, I added conditional navigation to the DonutTracker app and also added a test to verify that the flow works! You can check out the solution code here.

With conditional navigation, the donut tracker app will trigger the one time flow to take the users to the selection fragment when they launch the app for the first time. If the user chooses to disable the coffee tracker, the app removes the coffeeList from the navigation menu.

With this the coffee tracker functionality is complete! In the upcoming articles we’ll learn how to use nested graphs and modularize this app.

--

--