导航与依赖注入(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,不跟别人共享。
你需要做两件事:
- 给 ViewModel 加上
@HiltViewModel注解 - 在 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 作用域最大,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 的创建和销毁,那代码得多乱?
好了,今天的内容就到这里。记住一句话:作用域选对了,代码就成功了一半。