第9章:Hilt与Navigation——Navigation组件中的依赖注入、NavGraphViewModels的配合
Navigation组件现在几乎是Android项目的标配了。但有个问题一直困扰着很多人——Fragment之间的依赖注入,到底该怎么搞?
我个人习惯把Navigation和Hilt的配合分成三个层次:Fragment级别的注入、NavGraph作用域的共享、以及NavBackStackEntry的生命周期管理。搞懂了这三层,你基本就能应对90%的场景了。
9.1 基础配置:让Hilt认识Navigation
首先,你得在build.gradle里加上这个依赖:
// Hilt + Navigation 集成
implementation 'androidx.hilt:hilt-navigation-fragment:1.0.0'
嗯,就这么一行。但很多人会漏掉一个关键步骤——Fragment必须用@AndroidEntryPoint注解。我记得有一次排查了半天,发现有个Fragment忘记加注解,结果注入死活不生效。
核心原则:Navigation中的每个Fragment,只要涉及依赖注入,都必须标注@AndroidEntryPoint。包括DialogFragment。
9.2 Fragment级别的注入:最基础的操作
先看一个最简单的例子。假设我们有个UserProfileFragment,需要注入一个UserRepository:
@AndroidEntryPoint
class UserProfileFragment : Fragment() {
@Inject
lateinit var userRepository: UserRepository
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
// 直接使用 userRepository
val user = userRepository.getUser()
}
}
这跟普通的Fragment注入没什么区别。但Navigation的坑在于——Fragment的创建方式变了。Navigation是通过NavHostFragment来创建Fragment的,如果你用了FragmentFactory自定义创建逻辑,可能会跟Hilt的注入机制冲突。
避坑指南:我曾经在一个项目里同时用了Hilt和自定义的FragmentFactory,结果Fragment的注入时好时坏。后来发现,Hilt的@AndroidEntryPoint已经内置了FragmentFactory的支持,你不需要再手动设置。如果你非要自定义,记得继承HiltFragmentFactory。
9.3 NavGraphViewModels:共享作用域的正确姿势
这才是本章的重头戏。Navigation组件里,多个Fragment可能需要共享同一个ViewModel。传统的做法是用activityViewModels(),但这样ViewModel的作用域是整个Activity,太宽了。
更好的做法是:把ViewModel的作用域限定在某个NavGraph里。这样,当用户离开这个导航图时,ViewModel自动销毁,不会造成内存泄漏。
Hilt提供了hiltNavGraphViewModels()来实现这个需求:
// 在Fragment中获取NavGraph作用域的ViewModel
@AndroidEntryPoint
class UserListFragment : Fragment() {
private val viewModel: UserListViewModel by hiltNavGraphViewModels(R.id.user_graph)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewModel.loadUsers()
}
}
注意这里的R.id.user_graph,它对应的是navigation目录下的nav_graph.xml中的id:
<navigation
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/user_graph"
app:startDestination="@id/userListFragment">
<fragment
android:id="@+id/userListFragment"
android:name=".ui.UserListFragment" />
<fragment
android:id="@+id/userDetailFragment"
android:name=".ui.UserDetailFragment" />
</navigation>
这样,UserListFragment和UserDetailFragment就能共享同一个UserListViewModel实例了。而且当导航到其他NavGraph时,这个ViewModel会被自动清理。
个人经验:我建议把NavGraph的粒度控制在「一个完整的功能模块」。比如用户模块一个图、订单模块一个图。这样ViewModel的作用域刚好覆盖整个功能流程,既不会太小(每个Fragment一个),也不会太大(整个Activity一个)。
9.4 带参数的NavGraphViewModels
有时候ViewModel需要接收参数,比如用户ID。Hilt的@HiltViewModel配合SavedStateHandle可以优雅地解决这个问题:
@HiltViewModel
class UserDetailViewModel @Inject constructor(
private val userRepository: UserRepository,
private val savedStateHandle: SavedStateHandle
) : ViewModel() {
val userId: String = savedStateHandle.get<String>("userId") ?: ""
fun loadUserDetail() {
viewModelScope.launch {
val user = userRepository.getUserById(userId)
// 更新UI状态
}
}
}
然后在Fragment中:
@AndroidEntryPoint
class UserDetailFragment : Fragment() {
private val viewModel: UserDetailViewModel by hiltNavGraphViewModels(R.id.user_graph)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
// 参数会自动从Navigation的arguments中获取
viewModel.loadUserDetail()
}
}
这里有个细节:SavedStateHandle会自动从Navigation的arguments中读取参数。你只需要在nav_graph.xml中定义好argument:
<fragment
android:id="@+id/userDetailFragment"
android:name=".ui.UserDetailFragment">
<argument
android:name="userId"
app:argType="string" />
</fragment>
说白了,Hilt帮你把Navigation的参数和ViewModel的初始化无缝衔接起来了。你不需要手动传参,也不需要写Factory。
9.5 NavBackStackEntry的生命周期管理
这是最容易踩坑的地方。Navigation的每个NavBackStackEntry都有自己的生命周期,而Hilt的注入是基于这个生命周期的。
举个例子:如果你在Fragment A中获取了一个NavGraph作用域的ViewModel,然后导航到Fragment B(同一个NavGraph),它们共享同一个ViewModel。但如果你按返回键回到Fragment A,ViewModel还在——因为NavBackStackEntry还没被销毁。
那什么时候ViewModel会被销毁?当整个NavGraph的所有BackStackEntry都被弹出时。也就是说,如果你从NavGraph A导航到NavGraph B,那么NavGraph A的ViewModel会被清理。
关键理解:hiltNavGraphViewModels()的作用域是NavBackStackEntry,而不是Fragment本身。同一个NavGraph下的所有Fragment,只要它们属于同一个BackStack,就共享同一个ViewModel实例。
9.6 实战:一个完整的用户模块示例
我们来整合一下。假设有一个用户模块,包含用户列表和用户详情两个页面:
// 1. 定义ViewModel
@HiltViewModel
class UserViewModel @Inject constructor(
private val userRepository: UserRepository,
private val savedStateHandle: SavedStateHandle
) : ViewModel() {
private val _users = MutableStateFlow<List<User>>(emptyList())
val users: StateFlow<List<User>> = _users.asStateFlow()
val selectedUserId: String?
get() = savedStateHandle.get<String>("userId")
fun loadUsers() {
viewModelScope.launch {
_users.value = userRepository.getAllUsers()
}
}
}
// 2. 用户列表Fragment
@AndroidEntryPoint
class UserListFragment : Fragment() {
private val viewModel: UserViewModel by hiltNavGraphViewModels(R.id.user_graph)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewModel.loadUsers()
// 观察用户列表
viewModel.users.observe(viewLifecycleOwner) { users ->
// 更新UI
}
}
}
// 3. 用户详情Fragment
@AndroidEntryPoint
class UserDetailFragment : Fragment() {
private val viewModel: UserViewModel by hiltNavGraphViewModels(R.id.user_graph)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
// 直接使用viewModel中的selectedUserId
val userId = viewModel.selectedUserId ?: return
// 加载详情...
}
}
你看,两个Fragment共享同一个UserViewModel,数据同步完全不需要额外处理。这就是NavGraph作用域的魅力。
9.7 性能与内存的考量
最后聊点实际的。NavGraph作用域的ViewModel虽然方便,但也不能滥用。我见过有人把整个App的ViewModel都放在一个NavGraph里,那跟用activityViewModels()有什么区别?
我的建议是:
- 每个独立功能模块使用一个NavGraph
- 模块内的页面共享ViewModel
- 跨模块的数据通过EventBus或共享Repository来传递,不要用ViewModel
另外要注意:hiltNavGraphViewModels()内部会持有NavBackStackEntry的引用,如果你在ViewModel中做了耗时操作,记得用viewModelScope来管理协程,避免生命周期泄漏。
我曾经踩过的坑:在一个电商项目里,我把购物车的ViewModel放在了主NavGraph里。结果用户从首页到商品详情再到购物车,来回导航了几次后,ViewModel里堆积了大量过期的网络请求。后来改成每个功能模块独立的NavGraph,问题就解决了。说白了,作用域不是越大越好,合适才是关键。
9.8 本章小结
Navigation + Hilt的组合,核心就三点:
- @AndroidEntryPoint 是每个Fragment的标配
- hiltNavGraphViewModels() 实现NavGraph作用域的共享
- SavedStateHandle 自动处理Navigation参数
搞懂了这些,你就能在Navigation中游刃有余地使用依赖注入了。记住,依赖注入不是目的,让代码更清晰、更可维护才是。