导航与依赖注入(Hilt):在Navigation中使用Hilt、为Fragment注入ViewModel、为NavGraph提供作用域

依赖注入和导航,这两个东西放在一起,很多人第一反应就是「麻烦」。我刚开始接触 Navigation + Hilt 的时候,也踩过不少坑。说白了,导航组件本身不关心你的 ViewModel 是怎么来的,它只负责跳转。但实际项目中,我们总不能每个 Fragment 都手动去拿 ViewModel 吧?那也太原始了。

Hilt 的出现,说白了就是帮我们把「谁该拥有什么」这件事理清楚。尤其是在 Navigation 的场景下,Fragment 的生命周期本来就短,再加上导航图的嵌套,作用域一乱,内存泄漏或者数据错乱就找上门了。嗯,今天我们就来把这套东西捋顺。

核心要点:Navigation + Hilt 的组合,核心在于理解「作用域」—— Fragment 的作用域、NavGraph 的作用域、以及 Activity 的作用域。选对了,代码清爽;选错了,调试到怀疑人生。

1. 为什么 Navigation 需要 Hilt?

先问个问题:没有 Hilt 的时候,你怎么在 Fragment 里拿 ViewModel?

最常见的做法是:

class MyFragment : Fragment() {
    private val viewModel: MyViewModel by viewModels()
}

这样写没问题,但有个隐含的坑——viewModels() 默认绑定的是 Fragment 自己的生命周期。如果你的 Fragment 被重建了,ViewModel 还在,但如果你在多个 Fragment 之间共享同一个 ViewModel,就得靠 Activity 作用域或者 NavGraph 作用域。这时候,手动管理就变得很痛苦。

我在项目中遇到过这样一个场景:一个订单流程有三个 Fragment,分别负责选择商品、填写地址、确认支付。这三个 Fragment 需要共享同一个订单 ViewModel。如果每个 Fragment 都自己创建 ViewModel,数据就断了。当时我用的办法是让 Activity 持有 ViewModel,然后 Fragment 通过 activityViewModels() 获取。但这样有个问题——Activity 一旦重建,ViewModel 也跟着重建,用户填了一半的信息就丢了。

后来换成 Hilt + Navigation,一切都清爽了。Hilt 可以精确控制 ViewModel 的作用域,让它只存在于某个 NavGraph 的生命周期内。导航图销毁了,ViewModel 才销毁。这才是正确的做法。

2. 为 Fragment 注入 ViewModel

先看最简单的场景:一个 Fragment 自己用 ViewModel,不跟别人共享。

你需要做两件事:

  1. 给 ViewModel 加上 @HiltViewModel 注解
  2. 在 Fragment 里用 hiltViewModel() 获取

代码长这样:

@HiltViewModel
class MyViewModel @Inject constructor(
    private val repository: MyRepository
) : ViewModel() {
    // ...
}

@AndroidEntryPoint
class MyFragment : Fragment() {
    private val viewModel: MyViewModel by hiltViewModel()
}

注意看,@HiltViewModel 是 Hilt 专门为 ViewModel 提供的注解。它跟普通的 @ViewModelScoped 不一样,它会自动处理 ViewModel 的创建和缓存。你不需要写任何 Factory 代码。

小提示:我习惯在 ViewModel 的构造函数里直接注入 Repository,而不是在 Fragment 里注入 Repository 再传给 ViewModel。这样 ViewModel 的依赖关系更清晰,测试也更好写。

这里有个细节:hiltViewModel() 默认绑定的是当前 Fragment 的作用域。也就是说,如果 Fragment 被销毁了,ViewModel 也会被清除。如果你希望 ViewModel 在 Fragment 重建后仍然存活,那就得考虑作用域的问题了。

3. 为 NavGraph 提供作用域

这才是 Navigation + Hilt 的精髓所在。

假设你有一个导航图,里面包含三个 Fragment:A、B、C。你想让这三个 Fragment 共享同一个 ViewModel。怎么做?

答案是用 @HiltViewModel 加上 SavedStateHandle,然后在每个 Fragment 里用 hiltViewModel() 获取。但这里有个关键点:hiltViewModel() 可以接受一个 NavBackStackEntry 参数,用来指定 ViewModel 的作用域。

看代码:

// 在导航图里的任意 Fragment 中
val navController = findNavController()
val navGraphViewModel: SharedViewModel by hiltViewModel(
    viewModelStoreOwner = navController.getBackStackEntry(R.id.my_nav_graph)
)

这里 R.id.my_nav_graph 就是你的导航图 ID。这样获取到的 ViewModel,它的生命周期就跟这个导航图绑定。只要导航图没有被弹出栈,ViewModel 就一直存在。三个 Fragment 拿到的都是同一个实例。

注意:如果你在 Fragment A 里用 hiltViewModel() 获取了 NavGraph 作用域的 ViewModel,然后在 Fragment B 里又用同样的方式获取,它们确实是同一个实例。但如果你在 Fragment B 里不小心用了 by hiltViewModel() 不带参数,那就会创建一个新的、绑定到 Fragment B 自己的 ViewModel。这个坑我踩过,调试了半天才发现是两个不同的实例。

为了更直观地理解,我画了一张图:

Activity 作用域 (@ActivityScoped) 生命周期 = Activity 创建到销毁 适合:全局单例、网络客户端、数据库实例 NavGraph 作用域 (@HiltViewModel + NavBackStackEntry) 生命周期 = 导航图入栈到出栈 适合:多 Fragment 共享数据(订单流程、表单填写) Fragment 作用域 (默认 hiltViewModel()) 生命周期 = Fragment 创建到销毁 适合:单个 Fragment 的独立数据 包含 包含

从这张图可以看得很清楚:Activity 作用域最大,NavGraph 次之,Fragment 最小。选择哪个作用域,取决于你的数据需要存活多久。

4. 实战:多 Fragment 共享 ViewModel

我来给你展示一个完整的例子。假设我们有一个用户注册流程,包含三个 Fragment:输入手机号、输入验证码、设置密码。这三个 Fragment 需要共享一个 RegisterViewModel

首先,定义导航图:

<navigation 
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:id="@+id/register_nav_graph"
    app:startDestination="@id/phoneFragment">
    
    <fragment android:id="@+id/phoneFragment" ... />
    <fragment android:id="@+id/verifyFragment" ... />
    <fragment android:id="@+id/passwordFragment" ... />
</navigation>

然后,定义 ViewModel:

@HiltViewModel
class RegisterViewModel @Inject constructor(
    private val authRepository: AuthRepository,
    private val savedStateHandle: SavedStateHandle
) : ViewModel() {
    
    var phoneNumber: String?
        get() = savedStateHandle["phone"]
        set(value) { savedStateHandle["phone"] = value }
    
    var verifyCode: String?
        get() = savedStateHandle["code"]
        set(value) { savedStateHandle["code"] = value }
    
    fun register() {
        // 调用 authRepository 注册
    }
}

注意这里我用了 SavedStateHandle。它可以在进程被杀死后恢复数据,比单纯用 MutableLiveData 更安全。

最后,在每个 Fragment 里获取 ViewModel:

@AndroidEntryPoint
class PhoneFragment : Fragment() {
    
    private val registerViewModel: RegisterViewModel by hiltViewModel(
        viewModelStoreOwner = findNavController().getBackStackEntry(R.id.register_nav_graph)
    )
    
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        // 直接使用 registerViewModel
        registerViewModel.phoneNumber = "13800138000"
    }
}

其他两个 Fragment 写法完全一样。这样,三个 Fragment 操作的都是同一个 ViewModel 实例,数据天然共享。

避坑指南:我曾经在 getBackStackEntry() 里传错了导航图 ID,结果抛了个 IllegalArgumentException,说找不到这个导航图。后来我养成了一个习惯:在导航图 XML 里给每个导航图都加上 android:id,并且在代码里用 R.id 引用,不要硬编码字符串。

5. 作用域选择总结

说了这么多,最后给你一个决策表,方便你快速判断该用哪种作用域:

场景 推荐作用域 获取方式
单个 Fragment 独立数据 Fragment 作用域 by hiltViewModel()
同一导航图内多个 Fragment 共享数据 NavGraph 作用域 by hiltViewModel(viewModelStoreOwner = ...)
整个 Activity 内共享数据 Activity 作用域 by activityViewModels()@ActivityScoped
全局单例(如网络客户端) Singleton 作用域 @Singleton

嗯,这张表基本覆盖了日常开发中 90% 的场景。剩下的 10%,要么是嵌套导航图,要么是 DialogFragment 的特殊情况,那些我们以后再聊。

最后说一句:依赖注入不是银弹,但它确实能帮你把代码组织得更清晰。Navigation + Hilt 这套组合,说白了就是让「谁该拥有什么数据」这件事变得可预测、可测试。你想想看,如果每个 Fragment 都自己管理 ViewModel 的创建和销毁,那代码得多乱?

好了,今天的内容就到这里。记住一句话:作用域选对了,代码就成功了一半。


公众号:蓝海资料掘金营,微信deep3321