第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>

这样,UserListFragmentUserDetailFragment就能共享同一个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的组合,核心就三点:

  1. @AndroidEntryPoint 是每个Fragment的标配
  2. hiltNavGraphViewModels() 实现NavGraph作用域的共享
  3. SavedStateHandle 自动处理Navigation参数

搞懂了这些,你就能在Navigation中游刃有余地使用依赖注入了。记住,依赖注入不是目的,让代码更清晰、更可维护才是。

Hilt + Navigation 依赖注入架构图 NavGraph: user_graph (作用域边界) NavBackStackEntry 生命周期 UserListFragment @AndroidEntryPoint by hiltNavGraphViewModels() ← 注入 UserViewModel UserDetailFragment @AndroidEntryPoint by hiltNavGraphViewModels() ← 注入 UserViewModel 共享 UserViewModel (共享实例) @HiltViewModel + SavedStateHandle 作用域 = NavBackStackEntry,随导航图销毁 图例:虚线框 = 作用域边界 | 实线框 = 组件 | 双向箭头 = 共享关系
公众号:蓝海资料掘金营,微信deep3321