This is the second in the MAD Skills series on Navigation. This article is the second in the Navigation components series. If you want to review past releases, check out the link below:
- Overview of navigation components
- Navigate to the dialog box
- Use SafeArgs when navigating your application
- Use deep link navigation
- Build your first App Bundle
- NavigationUI is easy to understand
If you’d rather watch the video than read the article, check out this video.
An overview of the
Conditional navigation refers to the fact that when you are designing navigation for an application, you may need to take the user to one destination rather than another based on Conditional logic. For example, the user might follow a deep link to a destination that requires the user to log in, or you might provide different destinations in the game for the player to win or lose.
In the last article, I used NavigationUI for the bottom navigation of my application and added SelectionFragment to enable or disable coffee logging. However, whether we disable or enable the coffee logger, the user can navigate to the CoffeeList Fragment page, which seems rather illogical.
In this article, I’ll fix this by adding conditional navigation, and guide our users to make choices when they first enable the application. I’ll use the Datastore API to save the user’s selection and decide whether to display the coffeeList destination in the bottom navigation.
Preparation for using conditional navigation in your application
Here’s a quick review of the changes I’ve made since the last post:
- First of all, I added UserPreferencesRepository, it USES the DataStore API to save the user’s choice;
- To access the Repository, I also made some changes in the ViewModel factory classes and modified the way the DonutListViewModel and SelectionViewModel are constructed.
If you want to see specific changes, check out the repository. If you follow along with the article, you can also check out the code in the repository.
The application now has 3 different states:
- DONUT_ONLY: Indicates that the coffee recording function is disabled
- DONUT_AND_COFFEE: Means the user wants to record both donut and coffee consumption
- NOT_SELECTED: means the user has not yet made a choice and may be starting the application for the first time,Or the user may have a hard time making a decision🤷
Implement conditional navigation
I’m going to start implementing the conditional navigation in the SelectionFragment. First I get an instance of the SelectionViewModel so I can access the DataStore through it. I then observed the user’s selection and used it to restore the state of the check box. In order to save the user’s choice, I will be click the check box when the saveCoffeeTrackerSelection () to update the status.
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)
/ /...
Copy the code
Now it’s time to update the bottom TAB bar based on the user’s choices. If the user chooses to disable coffee records, there is only one donutList option left in the bottom TAB, which means that the bottom TAB can be safely removed. In MainActivity, I’ll add an Observer and update the Visibility of the bottom TAB bar. To do this, I’ll add an observer and update the visibility of BottomNavigation based on the user’s 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}}Copy the code
In the onCreate () :
val selectionViewModel: SelectionViewModel by viewModels {
SelectionViewModelFactory(
UserPreferencesRepository.getInstance(this)
)
}
selectionViewModel.checkCoffeeTrackerEnabled().observe(this) { s ->
setupMenu(s)
}
Copy the code
Running the application in its current state, you will find that enabling or disabling coffee records will add or remove the bottom TAB bar in the application accordingly. This looks great, but it would be even better if we automatically sent it to users for selection the first time they run the application.
DonutList is the default Fragment, which is our starting destination, which means the app always starts from DonutList, and I check to see if the user has made a selection before, and if not, I trigger the navigation to SelectionFragment.
donutListViewModel.isFirstRun().observe(viewLifecycleOwner) { s ->
if (s == UserPreferencesRepository.Selection.NOT_SELECTED) {
val navController = findNavController()
navController.navigate(
DonutListDirections.actionDonutListToSelectionFragment()
)
}
}
Copy the code
Before testing this feature, I needed to uninstall the application from the device to make sure I didn’t save preferences left over from the last run. Now when I run the application, it’s going to navigate to the SelectionFragment. Subsequent app launches will remember my choice and navigate me to the correct starting destination.
That’s it! We added conditional navigation to the DonutTracker application. But how do we test this process? Uninstalling the application or deleting the application data before each test run is not optimal. That’s what Testing is all about!
Test the navigation
I created a test class called OneTimeFlowTest in the androidTest folder. Then I created a Test method called testFirstRun() and annotated it with @test. Now I’m going to implement the test. I created TestNavHostController() using applicationContext, and I also set the nav_graph in my application for the testNavigationController instance I just created.
@Test
fun testFirstRun(a) {
// Create a simulated NavController
val mockNavController = TestNavHostController(
ApplicationProvider.getApplicationContext()
)
mockNavController.setGraph(R.navigation.nav_graph)
/ /...
}
Copy the code
Now that mockNavigationController is available, it’s time to create the test scenario. To do this, I launch the application with the DonutList Fragment and set up the mockNavigationController instance I created earlier. Then see if the application 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) }Copy the code
Now I run the test and wait for the results… The test passed!
â–³ Test navigation
summary
In this article, I added conditional navigation to the DonutTracker application, as well as tests to verify that the process is working properly — the solution code.
With conditional navigation, when the user first launches the DonutTracker application, the application triggers a process to navigate the user to the SelectionFragment. If the user chooses to disable the coffee logger, the application will remove the CoffeeList from the navigation menu.
At this point, the coffee recording function is complete! In the next article, we’ll learn how to use Nested graphs and modularize the application.