这是第二个关于导航 (Navigation) 的 MAD Skills 系列,本文是导航组件系列的第二篇文章,如果您想回顾过去发布的内容,请参考下面链接查看:
如果您更倾向于观看视频而非阅读文章,请查看 这个视频 内容。
概述
条件导航 (Conditional navigation) 指的是在为应用设计导航时,您可能需要基于条件逻辑将用户转到某一个目的地而非另一个。例如,用户可能会跟随深层链接前往一个需要用户登录的目的地,或者您可能会在游戏中针对玩家的输赢提供不同的目的地。
在 上一篇文章 中,我使用 NavigationUI 实现了应用的底部导航,并增加了 SelectionFragment 来启用或禁用咖啡记录功能。然而,无论我们禁用或启用咖啡记录器,用户都可以导航到 CoffeeList Fragment 页面,这看起来不太符合逻辑。
在本文中,我将通过添加条件导航来修复这个问题,并且当用户首次启用应用时指导我们的用户做出选择。我将使用 Datastore API 来保存用户的选择,并据此决定是否在底部导航中展示 coffeeList 目的地。
在应用中使用条件导航的准备工作
这是自上一篇文章以来我所做 修改 的快速回顾:
- 首先,我添加了 UserPreferencesRepository,它使用 DataStore API 来保存用户的选择;
- 为了访问该 Repository,我在各 ViewModel 工厂类中也做出了些许改变,并且修改了 DonutListViewModel 和 SelectionViewModel 的构造方式。
如果您想查看具体的修改内容,请查 阅该仓库。如果您跟着文章一起操作,也可以检出仓库中的代码。
现在应用具有 3 种不同的状态:
- DONUT_ONLY: 意味着用户禁用了咖啡记录功能
- DONUT_AND_COFFEE: 意味着用户想同时记录甜甜圈和咖啡的消费情况
- NOT_SELECTED: 意味着用户还没有做出选择而且有可能是第一次启动应用,或者用户也许很难做出决定
实现条件导航
我将在 SelectionFragment 中开始实现条件导航。首先我获取了 SelectionViewModel 的一个实例,因此我可以通过它访问 DataStore。然后,我观察 (Observe) 了用户的选择并以此来恢复复选框的状态。为了保存用户的选择,我将在复选框被点击时调用 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)
//...
现在是时候根据用户的选择来更新底部标签栏了。如果用户选择禁用咖啡记录,底部标签栏中便只剩下一个 donutList
选项了,这意味着我们可以安全的移除底部标签栏。在 MainActivity
中,我将添加观察者 (Observer) 并且更新底部标签栏的可见性 (Visibility)。为了实现这一目的,我将添加一个观察者并且根据用户的选择来更新 BottomNavigation
的可见性。
private fun setupMenu(
selection: UserPreferencesRepository.Selection
) {
val bottomNav = findViewById(R.id.bottom_nav_view)
bottomNav.isVisible = when (selection) {
UserPreferencesRepository.Selection.DONUT_AND_COFFEE -> true
else -> false
}
}
在 onCreate() 中:
val selectionViewModel: SelectionViewModel by viewModels {
SelectionViewModelFactory(
UserPreferencesRepository.getInstance(this)
)
}
selectionViewModel.checkCoffeeTrackerEnabled().observe(this) { s ->
setupMenu(s)
}
在当前状态下运行应用,您会发现启用或禁用咖啡记录将对应地在应用中添加或移除底部标签栏。这看起来很棒,但是如果我们在用户首次运行应用时自动将其发送给用户进行选择,那会更好。
DonutList
是默认的 Fragment,也是我们的起始目的地,这意味着应用总是从 DonutList
启动,我会检查用户之前是否做出过选择,如果没有,则触发导航至 SelectionFragment
。
donutListViewModel.isFirstRun().observe(viewLifecycleOwner) { s ->
if (s == UserPreferencesRepository.Selection.NOT_SELECTED) {
val navController = findNavController()
navController.navigate(
DonutListDirections.actionDonutListToSelectionFragment()
)
}
}
在测试该功能前,我需要从设备上卸载应用,以确保不会保存上次运行时遗留下的偏好设置。现在当我运行应用时,它会导航至 SelectionFragment
。后续应用的启动将会记住我做出的选择并将我导航至正确的起始目的地。
就是如此!我们在 DonutTracker 应用中添加了条件导航。但是我们如何测试该流程?每次运行测试前都卸载应用或删除应用数据的话并不是最理想的效果。这就是测试 (Testing) 所要解决的问题!
测试导航
我在 androidTest 文件夹下创建了一个名为 OneTimeFlowTest
的测试类。然后我创建了一个名为 testFirstRun()
的测试方法,并为它添加 @Test
注解。现在我开始实现该测试。我使用 applicationContext
创建了 TestNavHostController()
,我也为刚创建的 testNavigationController
实例设置了应用中的 nav_graph
。
@Test
fun testFirstRun() {
// 创建模拟的 NavController
val mockNavController = TestNavHostController(
ApplicationProvider.getApplicationContext()
)
mockNavController.setGraph(R.navigation.nav_graph)
//...
}
至此,mockNavigationController
已经可以使用了,现在是创建测试场景的时候了。要做到这一点,我用 DonutList
Fragment 启动应用并设置我之前创建的 mockNavigationController
实例。然后查看应用是否像预期那样自动导航至 SelectionFragment
。
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)
}
现在我运行该测试并等待结果... 测试顺利通过!
△ 测试导航
小结
在本文中,我在 DonutTracker 应用中添加了条件导航,同时也添加了测试来验证流程是否正常工作——解决方案代码。
通过条件导航,当用户首次启动 DonutTracker 应用时,应用将触发一次流程,将用户导航至 SelectionFragment。如果用户选择禁用咖啡记录器,应用将从导航菜单中移除咖啡列表 (CoffeeList
)。
至此,咖啡记录功能已经完整了!在接下来的文章中,我们将学习如何使用嵌套图 (Nested graphs) 并将模块化该应用。