13、Hilt KTX:Hilt的ViewModel注入与@HiltViewModel注解的KTX写法
依赖注入这个话题,在Android圈子里讨论了好几年了。从最早的Dagger 2,到后来Google官方推出的Hilt,再到现在的KTX写法,每一步都在朝着「让开发者少写样板代码」这个方向走。
我个人最早接触Hilt的时候,其实有点抵触。毕竟Dagger 2那一套注解处理器、Component、Module的写法,已经够让人头疼了。但用了一段时间Hilt之后,我不得不承认——嗯,这玩意儿确实香。尤其是配合KTX扩展之后,ViewModel的注入变得异常简洁。
为什么需要Hilt KTX?
先说说痛点。在Hilt出现之前,我们要在ViewModel里注入依赖,通常需要写一个ViewModelFactory。代码量不小,而且每个ViewModel都要配一个Factory,维护起来很烦。
举个例子,假设你有一个UserRepository需要注入到UserViewModel里。传统写法是这样的:
class UserViewModel @Inject constructor(
private val userRepo: UserRepository
) : ViewModel() {
// ...
}
@HiltViewModel
class UserViewModel @Inject constructor(
private val userRepo: UserRepository
) : ViewModel() {
// ...
}
看到区别了吗?@HiltViewModel注解加上之后,你连Factory都不用写了。Hilt会自动帮你生成一个ViewModelFactory,并且在Activity或Fragment里通过by viewModels()就能拿到实例。
核心变化:从「手动写Factory」到「注解驱动」,Hilt KTX把ViewModel注入的门槛降到了最低。
@HiltViewModel 注解的工作原理
这个注解是Hilt 1.0之后才加入的。我记得刚发布的时候,社区里一片叫好。为什么?因为它解决了ViewModel注入最麻烦的一环——生命周期管理。
ViewModel本身是有生命周期的,它和Activity/Fragment绑定。如果直接用Dagger的@Inject注解,你拿到的ViewModel实例可能不是同一个,或者生命周期管理出问题。@HiltViewModel注解告诉Hilt:这个ViewModel需要由Hilt来管理它的创建和销毁。
具体来说,Hilt会做以下几件事:
- 自动生成一个
HiltViewModelFactory,绑定到当前组件的ViewModelStoreOwner - 确保ViewModel的构造函数参数都能从Hilt的依赖图中获取
- ViewModel的生命周期与宿主组件保持一致,不会出现内存泄漏
说白了,你只需要在ViewModel类上加上@HiltViewModel,然后在构造函数里用@Inject标注,剩下的交给Hilt就行。
KTX写法:by viewModels() 与 hiltViewModel()
有了@HiltViewModel注解,接下来就是在Activity或Fragment里获取ViewModel了。这里KTX提供了两个非常方便的扩展函数:
by viewModels():标准的KTX扩展,用于获取ViewModel实例by hiltViewModel():Hilt专用的扩展,会自动使用HiltViewModelFactory
我建议你在使用Hilt的项目中,统一用by hiltViewModel()。为什么呢?因为by viewModels()默认使用的是系统默认的ViewModelProvider.Factory,它不知道Hilt的存在。如果你没有手动配置Factory,就会报错。
来看一个完整的例子:
// ViewModel层
@HiltViewModel
class DashboardViewModel @Inject constructor(
private val userRepo: UserRepository,
private val analyticsRepo: AnalyticsRepository
) : ViewModel() {
val userData = userRepo.getUserData()
val analyticsData = analyticsRepo.getAnalytics()
}
// Fragment层
@AndroidEntryPoint
class DashboardFragment : Fragment() {
private val viewModel: DashboardViewModel by hiltViewModel()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
// 直接使用viewModel
viewModel.userData.observe(viewLifecycleOwner) { user ->
// 更新UI
}
}
}
注意看,Fragment上需要加@AndroidEntryPoint注解。这个注解告诉Hilt:这个Fragment需要参与依赖注入。如果没有这个注解,by hiltViewModel()会找不到Hilt的Factory。
避坑指南:我曾经在一个项目里忘了给Fragment加@AndroidEntryPoint,结果运行时直接崩溃,报错信息是「No injectable factory for ViewModel」。排查了半天才发现是这个问题。所以,请务必检查你的Activity和Fragment是否都标注了正确的入口注解。
多模块项目中的ViewModel注入
在实际项目中,我们通常会把代码拆成多个模块。比如一个电商App,可能有app、core、feature-home、feature-cart等模块。这时候ViewModel的注入就需要考虑跨模块的依赖关系。
Hilt的解决方案是:每个模块都可以定义自己的ViewModel,只要它们所在的模块依赖了Hilt,并且ViewModel类上标注了@HiltViewModel,就可以正常工作。
举个例子,假设feature-home模块里有一个HomeViewModel,它需要注入core模块里的ProductRepository:
// core模块
@Module
@InstallIn(SingletonComponent::class)
object CoreModule {
@Provides
@Singleton
fun provideProductRepository(): ProductRepository {
return ProductRepositoryImpl()
}
}
// feature-home模块
@HiltViewModel
class HomeViewModel @Inject constructor(
private val productRepo: ProductRepository
) : ViewModel() {
// ...
}
只要feature-home模块在build.gradle里依赖了core模块,Hilt就能自动找到ProductRepository的提供方式。这就是Hilt的「编译时依赖图」的威力。
ViewModel与SavedStateHandle的配合
还有一个常见的场景:ViewModel需要保存状态,比如用户输入的表单数据。Hilt KTX对SavedStateHandle的支持也很自然。
@HiltViewModel
class FormViewModel @Inject constructor(
private val savedStateHandle: SavedStateHandle,
private val formRepo: FormRepository
) : ViewModel() {
fun saveField(key: String, value: String) {
savedStateHandle[key] = value
}
fun getField(key: String): String? {
return savedStateHandle[key]
}
}
你不需要手动创建SavedStateHandle,Hilt会自动注入它。而且这个Handle的生命周期和ViewModel绑定,进程被杀死后也能恢复数据。
小技巧:如果你在ViewModel里用了SavedStateHandle,建议在构造函数里把它放在第一个参数位置。这不是强制的,但算是一种约定俗成的写法,方便其他人阅读代码时一眼就能看到状态管理的部分。
SVG:Hilt ViewModel注入流程
下面这张图展示了从注解到最终获取ViewModel实例的完整流程:
常见问题与避坑
在实际使用中,有几个问题我经常遇到,这里列出来供你参考:
| 问题 | 原因 | 解决方案 |
|---|---|---|
| ViewModel无法创建,报错No injectable factory | Fragment/Activity未加@AndroidEntryPoint |
检查所有宿主组件是否标注了正确的入口注解 |
| ViewModel的依赖无法解析 | 依赖未在Hilt Module中提供,或作用域不匹配 | 检查Module的@InstallIn和@Provides配置 |
| 多个ViewModel共享同一个依赖实例 | 依赖的作用域设置不当 | 使用@Singleton或@ViewModelScoped控制作用域 |
| 编译时注解处理器报错 | Hilt版本与AGP版本不兼容 | 检查Hilt版本号,建议使用与AGP匹配的最新稳定版 |
我曾经踩过的坑:有一次我把@HiltViewModel写成了@HiltViewModle(少了个l),编译通过了,但运行时ViewModel一直创建失败。排查了半天才发现是拼写错误。这种低级错误真的很浪费时间,建议你在写注解时多用IDE的自动补全功能。
总结
Hilt KTX对ViewModel注入的优化,说白了就是两件事:@HiltViewModel注解和by hiltViewModel()扩展函数。前者让ViewModel的构造函数注入变得简单,后者让获取ViewModel实例变得优雅。
我个人觉得,这套组合拳最大的价值在于「约定优于配置」。你不需要写Factory,不需要手动管理生命周期,不需要担心作用域问题。Hilt在编译时就把这些事情都安排好了。
如果你还在用传统的ViewModelFactory写法,我建议你尽快迁移到Hilt KTX。迁移成本很低,但带来的开发效率提升是实实在在的。