第7章:Hilt与ViewModel:@HiltViewModel、ViewModel注入的最佳实践

ViewModel 的依赖注入,可以说是 Hilt 在日常开发中最常用的场景之一。我记得刚接触 DI 那会儿,最头疼的就是 ViewModel 里要拿个 Repository,还得自己写个 ViewModelFactory,代码又臭又长。后来 Hilt 出了 @HiltViewModel,这问题才算真正解决。

说白了,Hilt 帮我们把 ViewModel 的创建过程接管了。你不需要再手动写 Factory,也不需要操心 ViewModel 的生命周期。但这里头有几个坑,我踩过,今天一并说清楚。

7.1 @HiltViewModel 注解:到底解决了什么问题?

先看一个最直观的对比。没有 Hilt 的时候,你要这样写:

// 传统方式:自己写 Factory
class MyViewModel(
    private val repo: MyRepository,
    private val analytics: AnalyticsService
) : ViewModel() {

    class Factory(
        private val repo: MyRepository,
        private val analytics: AnalyticsService
    ) : ViewModelProvider.Factory {
        override fun <T : ViewModel> create(modelClass: Class<T>): T {
            return MyViewModel(repo, analytics) as T
        }
    }
}

// 在 Activity 里:
val factory = MyViewModel.Factory(repo, analytics)
val vm = ViewModelProvider(this, factory).get(MyViewModel::class.java)

嗯,你想想看,每加一个依赖,Factory 就得改一次。项目大了以后,光维护这些 Factory 就能把人逼疯。

@HiltViewModel 之后:

@HiltViewModel
class MyViewModel @Inject constructor(
    private val repo: MyRepository,
    private val analytics: AnalyticsService
) : ViewModel() {
    // 直接写业务逻辑,不用管 Factory
}

在 Activity 或 Fragment 里,直接用 by viewModels() 或者 hiltViewModel() 就能拿到实例。Hilt 在背后自动生成了 Factory,并且把依赖链都处理好了。

核心要点: @HiltViewModel 注解 + @Inject constructor,这两个缺一不可。少了任何一个,Hilt 都不会帮你自动创建 ViewModel。

7.2 ViewModel 注入的最佳实践

光会用注解还不够,实际项目里怎么组织代码,怎么避免踩坑,这才是关键。我把自己这几年总结的经验列出来。

7.2.1 依赖粒度:别把整个 UseCase 塞进去

我在项目中遇到过这样的代码:

// ❌ 反面案例
@HiltViewModel
class OrderViewModel @Inject constructor(
    private val orderUseCase: OrderUseCase  // 这个 UseCase 里包含了十几个方法
) : ViewModel()

这样做的问题在于:ViewModel 的依赖变得很重。你只是要查一个订单详情,却把整个订单模块的 UseCase 都拉进来了。单元测试的时候,mock 起来也麻烦。

我建议的做法是:

// ✅ 推荐做法:按功能拆分
@HiltViewModel
class OrderDetailViewModel @Inject constructor(
    private val getOrderDetailUseCase: GetOrderDetailUseCase,
    private val trackOrderUseCase: TrackOrderUseCase
) : ViewModel()

说白了,每个 ViewModel 只依赖它真正需要的东西。这样测试好写,代码也清晰。

7.2.2 SavedStateHandle:状态恢复的正确姿势

进程被杀后恢复状态,这是 Android 的老大难问题。Hilt 对 SavedStateHandle 的支持很到位,你直接在构造函数里声明就行:

@HiltViewModel
class ProfileViewModel @Inject constructor(
    private val savedStateHandle: SavedStateHandle,
    private val userRepo: UserRepository
) : ViewModel() {

    private val userId: String = savedStateHandle.get("userId") ?: ""

    fun loadProfile() {
        viewModelScope.launch {
            val profile = userRepo.getProfile(userId)
            savedStateHandle.set("profile", profile)
        }
    }
}

这里有个细节要注意:SavedStateHandle 不需要你手动提供,Hilt 会自动注入。但前提是——你的 ViewModel 必须是通过 @HiltViewModel 创建的。如果你自己 new 了一个 ViewModel,那 SavedStateHandle 就是空的。

我曾经踩过的坑: 在 Fragment 里用 ViewModelProvider(this).get(MyViewModel::class.java) 而不是 hiltViewModel(),结果 SavedStateHandle 一直拿不到数据。排查了半天才发现是获取方式不对。

7.2.3 作用域:ViewModel 里的依赖该不该用 @Singleton?

这个问题经常有人问。我的建议是:ViewModel 里的依赖,尽量用 @ViewModelScoped 或者不加作用域

为什么?

  • @Singleton:全局单例,整个应用生命周期内只创建一次。如果 ViewModel 里依赖了一个很大的数据缓存,用 @Singleton 没问题。但如果是跟用户会话相关的数据,用 @Singleton 就错了——用户切换后数据不会刷新。
  • @ViewModelScoped:这是 Hilt 1.0 之后引入的作用域。它保证依赖跟 ViewModel 同生命周期。ViewModel 销毁时,这个依赖也跟着销毁。我个人习惯,大部分场景用这个就够了。
  • 不加作用域:每次注入都创建新实例。适合无状态的工具类,比如 DateFormatter。

看个例子:

@Module
@InstallIn(ViewModelComponent::class)  // 注意这里
object ProfileModule {

    @Provides
    @ViewModelScoped
    fun provideProfileCache(): ProfileCache {
        return ProfileCache()
    }
}

嗯,这里 @InstallIn(ViewModelComponent::class) 也很关键。它告诉 Hilt:这个 Module 里的依赖只在 ViewModel 层级可用。如果你在 Activity 里想注入 ProfileCache,编译器会报错。这其实是好事——把依赖的作用域限制死了,避免滥用。

7.3 知识体系总览

下面这张图,把 Hilt 和 ViewModel 的关系梳理清楚了。你可以看到从注解声明到最终注入的完整链路。

Hilt + ViewModel 依赖注入链路 @HiltViewModel + @Inject Hilt 编译期生成 ViewModelFactory hiltViewModel() 获取实例,自动注入依赖 ViewModel 销毁 → @ViewModelScoped 依赖自动释放 声明层 编译层 运行层 销毁层

7.4 常见问题与避坑指南

问题 原因 解决方案
ViewModel 无法创建,报错 "cannot be created" 忘记加 @HiltViewModel 注解,或者构造函数没有 @Inject 检查注解是否齐全,确保 ViewModel 类上有 @HiltViewModel,构造函数有 @Inject
hiltViewModel() 编译报错 Fragment 没有用 @AndroidEntryPoint 注解 给 Fragment 加上 @AndroidEntryPoint,并确保依赖的 Activity 也有该注解
SavedStateHandle 拿不到数据 ViewModel 不是通过 Hilt 创建的,或者获取方式不对 使用 hiltViewModel() 而不是 ViewModelProvider(this)
依赖在 ViewModel 销毁后仍然存活 依赖使用了 @Singleton 作用域,但本应使用 @ViewModelScoped 评估依赖的生命周期需求,改用合适的作用域

一个小技巧: 如果你不确定某个依赖该用什么作用域,先不加。等发现它被频繁创建造成性能问题时,再考虑加 @ViewModelScoped 或 @Singleton。过早优化是万恶之源。

7.5 实战:一个完整的例子

最后,我把上面讲的东西串起来,给一个完整的实战代码。这是一个用户详情页的 ViewModel:

// 1. 定义 Repository(不加作用域,每次都是新实例)
class UserRepository @Inject constructor(
    private val api: UserApi
) {
    suspend fun getUser(id: String): User = api.fetchUser(id)
}

// 2. 定义 ViewModel
@HiltViewModel
class UserDetailViewModel @Inject constructor(
    private val savedStateHandle: SavedStateHandle,
    private val userRepo: UserRepository
) : ViewModel() {

    private val userId: String = savedStateHandle.get("userId") ?: ""

    private val _user = MutableStateFlow<User?>(null)
    val user: StateFlow<User?> = _user.asStateFlow()

    fun loadUser() {
        viewModelScope.launch {
            val result = userRepo.getUser(userId)
            _user.value = result
            savedStateHandle.set("user", result)
        }
    }
}

// 3. 在 Fragment 中使用
@AndroidEntryPoint
class UserDetailFragment : Fragment() {

    private val viewModel: UserDetailViewModel by hiltViewModel()

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        viewModel.loadUser()
        // 观察数据...
    }
}

这段代码里,UserRepository 没有加任何作用域注解,所以每次 ViewModel 创建时都会生成一个新的实例。如果你的 Repository 内部有缓存或者数据库连接,可以考虑加上 @Singleton。但记住我前面说的——先不加,有需要再加。

嗯,关于 Hilt 和 ViewModel 的配合,核心就是这些。说白了,Hilt 帮我们省掉了 Factory 的模板代码,但依赖的粒度、作用域的选择,这些设计上的事情还是得我们自己拿捏。多写几个项目,你自然就找到感觉了。


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