11、ViewModel与Navigation:在Fragment之间共享ViewModel、ViewModel的作用域、使用by navGraphViewModels()
说实话,ViewModel 和 Navigation 的搭配,是单 Activity 架构里最让我觉得「爽」的部分。你想想看,以前我们在多个 Fragment 之间传数据,要么靠接口回调,要么靠 SharedPreferences,要么靠 EventBus——这些方式用起来总有点「拧巴」的感觉。数据不同步、生命周期混乱、内存泄漏……踩过的坑能写一本小册子。
但 Navigation + ViewModel 这套组合拳打下来,很多问题就迎刃而解了。今天我就带你彻底搞懂:如何在 Fragment 之间共享 ViewModel,以及 ViewModel 的作用域到底是怎么回事。
ViewModel 的作用域:谁拥有它?
先问一个问题:ViewModel 什么时候会被销毁?
很多人会回答「当 Activity 销毁的时候」。没错,这是最基础的情况。但 Navigation 组件引入之后,事情变得更有意思了——ViewModel 的作用域可以精确到一个 Navigation 图(NavGraph)。
我画了一张图,帮你理清这几种作用域的关系:
看到这张图你应该明白了:Activity 作用域的 ViewModel 在整个 Activity 存活期间都存在,而 NavGraph 作用域的 ViewModel 只在你处于该导航图内时才存在。一旦你导航离开了这个 NavGraph(比如返回到了上一级图),这个 ViewModel 就会被清除。
核心要点:用 by viewModels() 获取的是 Activity 级别的 ViewModel,用 by navGraphViewModels() 获取的是 NavGraph 级别的 ViewModel。两者的生命周期不同,千万别搞混。
在 Fragment 之间共享 ViewModel
好,现在我们来解决一个实际问题:两个 Fragment 之间怎么共享数据?
举个例子,一个典型的「填写表单」流程:
- FragmentA:填写姓名和邮箱
- FragmentB:填写地址和电话
- FragmentC:确认并提交
这三个 Fragment 需要共享同一个数据模型。以前的做法是什么?把数据塞到 Bundle 里传来传去,或者写一个全局的单例。这两种方式我都用过,体验都不好——Bundle 传多了容易丢数据,单例又容易造成内存泄漏。
用 Navigation + ViewModel 就优雅多了:
// 1. 定义共享的 ViewModel
class FormViewModel : ViewModel() {
val name = MutableLiveData<String>()
val email = MutableLiveData<String>()
val address = MutableLiveData<String>()
val phone = MutableLiveData<String>()
fun submit() {
// 提交表单逻辑
}
}
// 2. 在 FragmentA 中使用
class FragmentA : Fragment() {
// 注意:这里用的是 navGraphViewModels
private val formViewModel: FormViewModel by navGraphViewModels(R.id.form_nav_graph)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
btnNext.setOnClickListener {
formViewModel.name.value = etName.text.toString()
formViewModel.email.value = etEmail.text.toString()
findNavController().navigate(R.id.action_a_to_b)
}
}
}
// 3. 在 FragmentB 中同样使用
class FragmentB : Fragment() {
private val formViewModel: FormViewModel by navGraphViewModels(R.id.form_nav_graph)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
// 直接读取 FragmentA 填写的数据
tvName.text = formViewModel.name.value
tvEmail.text = formViewModel.email.value
}
}
💡 我的经验:用 by navGraphViewModels() 时,记得传入正确的 NavGraph ID。我刚开始用的时候,经常传错 ID,结果 ViewModel 一直拿不到数据,排查了半天才发现是 ID 写错了。建议把 NavGraph ID 定义在 R.navigation 里,用的时候直接引用,别手写字符串。
by navGraphViewModels() 的底层原理
你可能会好奇:by navGraphViewModels() 到底是怎么工作的?
其实它的核心逻辑很简单:它从 NavBackStackEntry 中获取 ViewModelStore。每个 NavGraph 都有一个对应的 ViewModelStore,当 NavGraph 出栈时,这个 ViewModelStore 就会被清除。
我拆解一下它的源码逻辑(简化版):
// 伪代码,帮你理解原理
fun <VM : ViewModel> navGraphViewModels(navGraphId: Int): Lazy<VM> {
return lazy {
val navController = findNavController()
// 获取 NavGraph 对应的 back stack entry
val backStackEntry = navController.getBackStackEntry(navGraphId)
// 从该 entry 的 ViewModelStore 中获取 ViewModel
ViewModelProvider(backStackEntry).get(VM::class.java)
}
}
说白了,NavGraph 的 BackStackEntry 充当了 ViewModel 的「容器」。只要这个 BackStackEntry 还在返回栈里,ViewModel 就活着。一旦你导航到了其他 NavGraph,这个 BackStackEntry 被移除,ViewModel 也就被清除了。
⚠️ 我曾经踩过的坑:有一次我在 FragmentA 里用 by navGraphViewModels() 获取了 ViewModel,然后在 FragmentB 里也用同样的方式获取。但 FragmentB 属于另一个 NavGraph,结果两个 Fragment 拿到的不是同一个 ViewModel 实例。数据当然也就没共享成功。
教训:要共享 ViewModel 的 Fragment 必须在同一个 NavGraph 里。如果你不确定,可以在 ViewModel 的 init 块里打印日志,看看是不是同一个实例。
什么时候用 Activity 作用域?什么时候用 NavGraph 作用域?
这个问题我经常被问到。我个人的建议是这样的:
| 场景 | 推荐作用域 | 原因 |
|---|---|---|
| 多个 Fragment 共享表单数据 | NavGraph 作用域 | 离开该流程后自动清除,避免内存泄漏 |
| 全局登录状态 | Activity 作用域 | 整个 App 生命周期都需要,且多个 NavGraph 共用 |
| 底部导航栏各 Tab 的数据 | NavGraph 作用域 | 每个 Tab 独立,切换 Tab 时清除旧数据 |
| 购物车数据 | Activity 作用域 | 用户可能在多个页面操作购物车,需要全局共享 |
你看,其实没有绝对的「哪个更好」,关键看你的业务场景。我一般的原则是:能用 NavGraph 作用域就用 NavGraph 作用域,因为它更精确,生命周期更可控。只有确实需要跨 NavGraph 共享数据时,才考虑 Activity 作用域。
一个完整的实战例子
最后,我给你一个完整的例子,把今天讲的知识点串起来。假设我们有一个「用户注册」流程,包含三个步骤:
- Step1Fragment:填写用户名和密码
- Step2Fragment:填写个人资料
- Step3Fragment:确认并注册
这三个 Fragment 都在 register_nav_graph 这个 NavGraph 里。
// 共享的 ViewModel
class RegisterViewModel : ViewModel() {
val username = MutableLiveData<String>()
val password = MutableLiveData<String>()
val nickname = MutableLiveData<String>()
val avatar = MutableLiveData<String>()
private val _isRegistering = MutableLiveData<Boolean>()
val isRegistering: LiveData<Boolean> = _isRegistering
fun register() {
viewModelScope.launch {
_isRegistering.value = true
// 调用注册接口
delay(2000) // 模拟网络请求
_isRegistering.value = false
}
}
}
// Step1Fragment
class Step1Fragment : Fragment() {
private val viewModel: RegisterViewModel by navGraphViewModels(R.id.register_nav_graph)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
btnNext.setOnClickListener {
viewModel.username.value = etUsername.text.toString()
viewModel.password.value = etPassword.text.toString()
findNavController().navigate(R.id.action_step1_to_step2)
}
}
}
// Step2Fragment
class Step2Fragment : Fragment() {
private val viewModel: RegisterViewModel by navGraphViewModels(R.id.register_nav_graph)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
// 可以直接读取 Step1 填写的数据
tvUsername.text = "用户名:${viewModel.username.value}"
btnNext.setOnClickListener {
viewModel.nickname.value = etNickname.text.toString()
viewModel.avatar.value = etAvatar.text.toString()
findNavController().navigate(R.id.action_step2_to_step3)
}
}
}
// Step3Fragment
class Step3Fragment : Fragment() {
private val viewModel: RegisterViewModel by navGraphViewModels(R.id.register_nav_graph)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
// 展示所有数据
tvSummary.text = """
用户名:${viewModel.username.value}
昵称:${viewModel.nickname.value}
头像:${viewModel.avatar.value}
""".trimIndent()
btnRegister.setOnClickListener {
viewModel.register()
}
// 观察注册状态
viewModel.isRegistering.observe(viewLifecycleOwner) { isRegistering ->
btnRegister.isEnabled = !isRegistering
progressBar.visibility = if (isRegistering) View.VISIBLE else View.GONE
}
}
}
关键点回顾:
- 三个 Fragment 通过
by navGraphViewModels(R.id.register_nav_graph)获取同一个 ViewModel 实例 - 数据在 Fragment 之间流转,不需要 Bundle 传递
- 当用户完成注册并离开
register_nav_graph时,ViewModel 自动清除,不会造成内存泄漏
嗯,这就是我今天想跟你分享的全部内容。ViewModel 和 Navigation 的配合,说白了就是「让数据跟着导航图走」。你只要搞清楚作用域,用对 by navGraphViewModels(),就能写出既清晰又健壮的代码。
下次你在 Fragment 之间传数据的时候,不妨试试这种方式。相信我,用习惯了你就回不去了。
公众号:蓝海资料掘金营,微信deep3321