This is the second MAD Skills series on navigation. This is the second article in the navigation component series. If you want to review the content published in the past, please refer to the following link:
- Overview of navigation components
- Navigate to dialog box
- Use SafeArgs when navigating through apps
- Use deep link navigation
- Build your first app bundle
- Easy to understand NavigationUI
If you prefer to watch videos rather than read articles, please check This video Content.
summary
Conditional navigation refers to that when designing navigation for applications, you may need to move users to one destination rather than another based on conditional logic. For example, users may follow deep links to a destination that requires users to log in, or you may provide different destinations for players to win or lose in the game.
stay Last article In, I used the navigation UI to realize the bottom navigation of the application, and added the selection fragment to enable or disable the coffee recording function. However, whether we disable or enable the coffee recorder, users can navigate to the coffee list fragment page, which seems illogical.
In this article, I will fix this problem by adding conditional navigation and guide our users to make choices when they first enable the application. I will use Datastore API To save the user's selection and decide whether to display the coffeeList destination in the bottom navigation.
Preparations for using conditional navigation in applications
This is what I have done since the last article modify Quick review of:
- First, I added UserPreferencesRepository , it uses the DataStore API to save the user's selection;
- In order to access the Repository, I also made some changes in each ViewModel factory class and modified it DonutListViewModel and SelectionViewModel The construction method of.
If you want to view the specific modifications, please check Read the warehouse . If you follow the article, you can also check out the code in the warehouse.
Now the application has three different states:
- DONUT_ONLY: it means that the user has disabled the coffee recording function
- DONUT_AND_COFFEE: it means that users want to record the consumption of doughnuts and coffee at the same time
- NOT_SELECTED: it means that the user has not made a choice, and it may be the first time to start the application, or it may be difficult for the user to make a decision 🤷
Realize conditional navigation
I'll start implementing conditional navigation in the SelectionFragment. First, I got an instance of SelectionViewModel, so I can access the DataStore through it. Then, I observed the user's selection and used it to restore the state of the check box. To save the user's selection, I will call saveCoffeeTrackerSelection() to update the status when the check box is clicked.
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's time to update the bottom tab bar based on the user's selection. If the user chooses to disable coffee records, there is only one donutList option left in the bottom tab, which means that we can safely remove the bottom tab. In MainActivity, I will add an observer and update the visibility of the bottom tab bar. To achieve 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 } }
In onCreate():
val selectionViewModel: SelectionViewModel by viewModels { SelectionViewModelFactory( UserPreferencesRepository.getInstance(this) ) } selectionViewModel.checkCoffeeTrackerEnabled().observe(this) { s -> setupMenu(s) }
Running the app in the current state, you will find that enabling or disabling coffee records will add or remove the bottom tab bar in the app accordingly. This looks great, but it would be better if we automatically send it to the user for selection when the user runs the application for the first time.
DonutList is the default Fragment and our starting destination, which means that the application is always launched from DonutList. I will check whether the user has made a choice before. If not, trigger navigation to SelectionFragment.
donutListViewModel.isFirstRun().observe(viewLifecycleOwner) { s -> if (s == UserPreferencesRepository.Selection.NOT_SELECTED) { val navController = findNavController() navController.navigate( DonutListDirections.actionDonutListToSelectionFragment() ) } }
Before testing this feature, I need to uninstall the app from the device to ensure that the preferences left over from the last run are not saved. Now when I run the application, it will navigate to the selection fragment. The launch of subsequent applications will remember my choices and navigate me to the correct starting destination.
That's it! We added conditional navigation to the DonutTracker application. But how do we test the process? Uninstalling the application or deleting the application data before running the test is not the best effect. This is the problem to be solved by testing!
Test navigation
I created a Test class named OneTimeFlowTest under the Android Test folder. Then I created a Test method called testFirstRun() and annotated it with @ Test. Now I begin to implement the Test. I created TestNavHostController() using applicationContext, and I also set NAV in the application for the newly created testNavigationController instance_ graph.
@Test fun testFirstRun() { // Create simulated NavController val mockNavController = TestNavHostController( ApplicationProvider.getApplicationContext() ) mockNavController.setGraph(R.navigation.nav_graph) //... }
Now that the mockNavigationController is ready to use, it's time to create a test scenario. To do this, I start the application with DonutList Fragment and set up the instance of mockNavigationController I created earlier. Navigate to the fragment as expected and see if it is applied automatically.
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 results The test passed successfully!
â–³ test navigation
Summary
In this article, I added conditional navigation to the DonutTracker application and tests to verify whether the process works properly—— Solution code.
Through conditional navigation, when the user starts the DonutTracker application for the first time, the application will trigger a process to navigate the user to the selection fragment. If the user chooses to disable the coffee recorder, the application will remove the coffee list from the navigation menu.
So far, the coffee recording function has been completed! In the next article, we will learn how to use nested graphs and modularize the application.