如何使用 @HiltViewModel 注解为 ViewModel 注入依赖?

解读

在国内一线/二线大厂的 Android 面试中,Hilt 作为 Google 官方 DI 框架,已经成为“基础必问”项。面试官抛出“怎么用 @HiltViewModel 注入依赖”,表面看是“API 怎么用”,实则想确认三件事:

  1. 你是否真的把 Hilt 跑通过(配置、注解、作用域、生命周期);
  2. 你是否理解 Hilt 与 Jetpack 生命周期组件的集成细节(ViewModel 的创建工厂、SavedStateHandle 来源、Activity/Fragment 作用域);
  3. 你是否具备“踩坑”经验,例如多模块、多进程、单元测试、混淆规则等国内项目常见痛点。
    回答时,务必“先给结论,再给代码,再给踩坑”,否则会被追问“为什么不用 @ViewModelInject”“SavedStateHandle 怎么来的”“单元测试怎么替换”。

知识点

  1. Hilt 的 ViewModel 专用注解:@HiltViewModel、@ViewModelScoped、@Assisted 等。
  2. Hilt 与 Jetpack 的集成原理:Hilt 通过自定义 ViewModelProvider.Factory(HiltViewModelFactory)把 ViewModel 实例化过程收拢到 DI 容器,从而支持构造函数注入。
  3. 作用域:@ViewModelScoped 保证 ViewModel 存活期间单例;@Singleton/@ActivityScoped 在 ViewModel 里无法直接使用。
  4. SavedStateHandle 的自动注入:Hilt 默认支持,无需 @Assisted 手工声明。
  5. 多模块场景:必须保证包含 ViewModel 的 module 被 @HiltAndroidApp 的 apk 模块依赖,否则编译期出现 “ViewModel 未绑定” 错误。
  6. 混淆与 R8:必须保留 HiltViewModel 的构造函数,规则已内置在 hilt-android 的 consumer-rules.pro,但国内渠道包经常二次裁剪,需检查 mapping。
  7. 单元测试:使用 hilt-android-testing + @UninstallModules 替换 Repo 实现;Robolectric 4.9+ 已支持 HiltViewModel 直接实例化。
  8. 与 Koin 的对比:Koin 在 1.5 之后也支持 ViewModel 注入,但大厂更倾向 Hilt,因为 Google 官方维护、与 Gradle 插件深度集成、对 Compose 支持更友好。

答案

  1. 环境准备
    项目级 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'
    }
    
  2. 定义可被注入的依赖

    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
    }
    
  3. 使用 @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()
    }
    
  4. 在 Activity/Fragment 中无工厂、无参数直接获取

    @AndroidEntryPoint
    class NewsActivity : ComponentActivity() {
    
        private val viewModel: NewsViewModel by viewModels()
    
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            setContent {
                NewsScreen(viewModel.uiState)
            }
        }
    }
    
  5. 编译期校验
    执行 ./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”,一定是作用域或模块安装位置错误。

  6. 国内渠道包踩坑
    某些厂商加固会 strip 掉构造函数,导致运行时 InstantiationException。在 proguard-rules.pro 显式保留:

    -keep @dagger.hilt.android.lifecycle.HiltViewModel class * {
        public <init>(...);
    }
    
  7. 单元测试替换依赖

    @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())
        }
    }
    

拓展思考

  1. 为什么 Hilt 要单独搞一个 @HiltViewModel,而不是直接 @Inject?
    答:ViewModel 的创建权在 Activity/Fragment 的 ViewModelStoreOwner,Hilt 必须提供自定义 Factory 才能介入;@Inject 仅对 Hilt 管理的普通类生效,无法直接用于系统组件。

  2. 如果构造函数里还需要 @Assisted 参数(如手动传入 id),怎么做?
    答:使用 @AssistedInject + @Assisted 关键字,并定义 AssistedViewModelFactory,Hilt 1.11 已支持,但国内大部分项目仍倾向把参数放到 SavedStateHandle 里,减少模板。

  3. 多进程场景下 ViewModel 还能用 Hilt 吗?
    答:ViewModel 本身依赖 Activity/Fragment 生命周期,多进程通常使用 android:process 的 Service 或 Provider,此时 ViewModel 不再适用,应降级到 @Singleton 作用域的 Repository 层,并通过 Room + WorkManager 做跨进程数据同步。

  4. 与 Compose Navigation 结合时,如何给 ViewModel 传入动态参数?
    答:使用 hiltViewModel() 函数,内部已自动从 NavBackStackEntry 里拿到 SavedStateHandle,因此路由参数直接塞进 NavGraph 的 arguments{} 即可,无需手写 Factory。

  5. 未来迁移到 KSP 编译器后,Hilt 的 APT 时代是否终结?
    答:Google 已发布 dagger.hilt.android.plugin 的 KSP 版本(2.48+),国内大型项目可逐步迁移,编译速度提升 20%~30%,但需同步升级 Kotlin 1.9+ 与 Room、Moshi 等周边库,否则会出现 “kaptKsp 共存” 冲突。