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)

我画了一张图,帮你理清这几种作用域的关系:

ViewModel 作用域对比 Activity 作用域 by viewModels() Fragment A 共享同一个 ViewModel Fragment B 共享同一个 ViewModel 生命周期 = Activity 生命周期 Activity 销毁 → ViewModel 清除 NavGraph 作用域 by navGraphViewModels() Fragment C 共享同一个 ViewModel Fragment D 共享同一个 ViewModel 生命周期 = NavGraph 出栈 离开 NavGraph → ViewModel 清除 区别

看到这张图你应该明白了: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 作用域。

一个完整的实战例子

最后,我给你一个完整的例子,把今天讲的知识点串起来。假设我们有一个「用户注册」流程,包含三个步骤:

  1. Step1Fragment:填写用户名和密码
  2. Step2Fragment:填写个人资料
  3. 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