如何使用 @HiltViewModel 注解为 ViewModel 注入依赖?
解读
在国内一线/二线大厂的 Android 面试中,Hilt 作为 Google 官方 DI 框架,已经成为“基础必问”项。面试官抛出“怎么用 @HiltViewModel 注入依赖”,表面看是“API 怎么用”,实则想确认三件事:
- 你是否真的把 Hilt 跑通过(配置、注解、作用域、生命周期);
- 你是否理解 Hilt 与 Jetpack 生命周期组件的集成细节(ViewModel 的创建工厂、SavedStateHandle 来源、Activity/Fragment 作用域);
- 你是否具备“踩坑”经验,例如多模块、多进程、单元测试、混淆规则等国内项目常见痛点。
回答时,务必“先给结论,再给代码,再给踩坑”,否则会被追问“为什么不用 @ViewModelInject”“SavedStateHandle 怎么来的”“单元测试怎么替换”。
知识点
- Hilt 的 ViewModel 专用注解:@HiltViewModel、@ViewModelScoped、@Assisted 等。
- Hilt 与 Jetpack 的集成原理:Hilt 通过自定义 ViewModelProvider.Factory(HiltViewModelFactory)把 ViewModel 实例化过程收拢到 DI 容器,从而支持构造函数注入。
- 作用域:@ViewModelScoped 保证 ViewModel 存活期间单例;@Singleton/@ActivityScoped 在 ViewModel 里无法直接使用。
- SavedStateHandle 的自动注入:Hilt 默认支持,无需 @Assisted 手工声明。
- 多模块场景:必须保证包含 ViewModel 的 module 被 @HiltAndroidApp 的 apk 模块依赖,否则编译期出现 “ViewModel 未绑定” 错误。
- 混淆与 R8:必须保留 HiltViewModel 的构造函数,规则已内置在 hilt-android 的 consumer-rules.pro,但国内渠道包经常二次裁剪,需检查 mapping。
- 单元测试:使用 hilt-android-testing + @UninstallModules 替换 Repo 实现;Robolectric 4.9+ 已支持 HiltViewModel 直接实例化。
- 与 Koin 的对比:Koin 在 1.5 之后也支持 ViewModel 注入,但大厂更倾向 Hilt,因为 Google 官方维护、与 Gradle 插件深度集成、对 Compose 支持更友好。
答案
-
环境准备
项目级 build.gradle(Groovy DSL 示例,国内镜像源):buildscript { dependencies { classpath "com.google.dagger:hilt-android-gradle-plugin:2.48" } }app 模块:
plugins { id 'dagger.hilt.android.plugin' id 'kotlin-kapt' } dependencies { implementation "com.google.dagger:hilt-android:2.48" kapt "com.google.dagger:hilt-compiler:2.48" implementation 'androidx.hilt:hilt-lifecycle-viewmodel:1.0.0-alpha03' // 已废弃,仅做兼容 kapt 'androidx.hilt:hilt-compiler:1.0.0' } -
定义可被注入的依赖
interface NewsRepo { suspend fun top(): List<String> } @Singleton class NewsRepoImpl @Inject constructor( private val api: NewsApi ) : NewsRepo { override suspend fun top() = api.top() } @Module @InstallIn(SingletonComponent::class) abstract class RepoModule { @Binds abstract fun bindNewsRepo(impl: NewsRepoImpl): NewsRepo } -
使用 @HiltViewModel 声明 ViewModel
@HiltViewModel class NewsViewModel @Inject constructor( private val repo: NewsRepo, private val savedStateHandle: SavedStateHandle // 可选,自动注入 ) : ViewModel() { private val _uiState = MutableStateFlow<UiState>(UiState.Loading) val uiState: StateFlow<UiState> = _uiState init { viewModelScope.launch { runCatching { repo.top() } .onSuccess { _uiState.value = UiState.Success(it) } .onFailure { _uiState.value = UiState.Error(it.message) } } } } sealed class UiState { object Loading : UiState() data class Success(val data: List<String>) : UiState() data class Error(val msg: String?) : UiState() } -
在 Activity/Fragment 中无工厂、无参数直接获取
@AndroidEntryPoint class NewsActivity : ComponentActivity() { private val viewModel: NewsViewModel by viewModels() override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { NewsScreen(viewModel.uiState) } } } -
编译期校验
执行./gradlew assembleDebug,Hilt 会在build/generated/hilt/component_sources下生成NewsViewModel_HiltModules.java,确保 ViewModel 已被绑定到ViewModelComponent。
若出现 “ViewModel has no @HiltViewModel annotation” 或 “Cannot be provided without an @Provides-annotated method”,一定是作用域或模块安装位置错误。 -
国内渠道包踩坑
某些厂商加固会 strip 掉构造函数,导致运行时InstantiationException。在proguard-rules.pro显式保留:-keep @dagger.hilt.android.lifecycle.HiltViewModel class * { public <init>(...); } -
单元测试替换依赖
@UninstallModules(RepoModule::class) @HiltAndroidTest class NewsViewModelTest { @get:Rule val hiltRule = HiltAndroidRule(this) @BindValue @JvmField val fakeRepo: NewsRepo = object : NewsRepo { override suspend fun top() = listOf("Fake") } @Test fun testFakeData() = runTest { val vm = NewsViewModel(fakeRepo, SavedStateHandle()) assertEquals("Fake", (vm.uiState.value as UiState.Success).data.first()) } }
拓展思考
-
为什么 Hilt 要单独搞一个 @HiltViewModel,而不是直接 @Inject?
答:ViewModel 的创建权在 Activity/Fragment 的 ViewModelStoreOwner,Hilt 必须提供自定义 Factory 才能介入;@Inject 仅对 Hilt 管理的普通类生效,无法直接用于系统组件。 -
如果构造函数里还需要 @Assisted 参数(如手动传入 id),怎么做?
答:使用@AssistedInject+@Assisted关键字,并定义AssistedViewModelFactory,Hilt 1.11 已支持,但国内大部分项目仍倾向把参数放到 SavedStateHandle 里,减少模板。 -
多进程场景下 ViewModel 还能用 Hilt 吗?
答:ViewModel 本身依赖 Activity/Fragment 生命周期,多进程通常使用android:process的 Service 或 Provider,此时 ViewModel 不再适用,应降级到@Singleton作用域的 Repository 层,并通过Room + WorkManager做跨进程数据同步。 -
与 Compose Navigation 结合时,如何给 ViewModel 传入动态参数?
答:使用hiltViewModel()函数,内部已自动从 NavBackStackEntry 里拿到 SavedStateHandle,因此路由参数直接塞进 NavGraph 的arguments{}即可,无需手写 Factory。 -
未来迁移到 KSP 编译器后,Hilt 的 APT 时代是否终结?
答:Google 已发布dagger.hilt.android.plugin的 KSP 版本(2.48+),国内大型项目可逐步迁移,编译速度提升 20%~30%,但需同步升级 Kotlin 1.9+ 与 Room、Moshi 等周边库,否则会出现 “kaptKsp 共存” 冲突。