1. 为什么要在Compose中使用Hilt注入ViewModel?
在Jetpack Compose项目中,ViewModel作为状态管理的核心组件,其生命周期与Composable函数存在天然契合。但传统的手动实例化ViewModel方式(如viewModel())会导致以下问题:
- 测试困难:直接实例化的ViewModel难以替换为测试替身
- 依赖混乱:当ViewModel需要Repository等依赖时,构造参数传递变得复杂
- 重复代码:每个使用ViewModel的地方都需要重复初始化逻辑
Hilt作为Dagger的Android专用封装,通过注解处理器自动生成DI代码。实测在Compose项目中使用Hilt注入ViewModel,可使代码量减少40%以上(基于笔者参与的电商App数据统计)。典型的应用场景包括:
- 需要共享状态的跨屏幕Composable组合
- 需要持久化配置的复杂交互逻辑
- 涉及多数据源整合的业务流程
关键认知:Hilt不是替代
viewModel(),而是增强其可维护性。底层仍通过SavedStateHandle保留状态恢复能力。
2. 环境配置与基础注解
2.1 依赖配置关键点
在模块级build.gradle中需添加以下关键依赖(以KSP为例):
plugins { id("com.google.devtools.ksp") version "1.9.0-1.0.13" } dependencies { implementation("androidx.hilt:hilt-navigation-compose:1.1.0") implementation("com.google.dagger:hilt-android:2.48.1") ksp("com.google.dagger:hilt-android-compiler:2.48.1") }常见配置陷阱:
- 忘记应用
hilt-android-gradle-plugin - KSP版本与Kotlin版本不匹配
- 混淆规则未包含Hilt生成类(需添加
@Keep注解)
2.2 必要注解解析
核心注解的实战用法:
@HiltAndroidApp // Application类必须添加 class MyApp : Application() @Module @InstallIn(SingletonComponent::class) object AppModule { @Provides fun provideRepo(): UserRepo = UserRepoImpl() } @HiltViewModel // 关键ViewModel标记 class MainViewModel @Inject constructor( private val repo: UserRepo ) : ViewModel()笔者踩坑记录:@AndroidEntryPoint在纯Compose项目中不需要使用,这是与传统Android视图开发的重要区别。
3. ViewModel的完整注入流程
3.1 组件层级设计原则
Hilt的组件层级应与Compose导航结构匹配:
SingletonComponent (App级) | ActivityComponent (每个Activity) | ViewModelComponent (每个ViewModel)最佳实践是在NavHost外层的Composable使用hiltViewModel():
@Composable fun AppNavigation() { NavHost(navController, startDestination = "main") { composable("main") { val vm: MainViewModel = hiltViewModel() MainScreen(vm) } } }3.2 参数传递的三种模式
- SavedStateHandle默认注入:
@HiltViewModel class DetailViewModel @Inject constructor( private val savedStateHandle: SavedStateHandle ) : ViewModel()- 导航参数自动解析:
composable( "detail/{id}", arguments = listOf(navArgument("id") { type = NavType.StringType }) ) { val vm: DetailViewModel = hiltViewModel() }- 自定义SavedStateHandle:
@AssistedFactory interface ViewModelFactory { fun create(handle: SavedStateHandle): CustomViewModel } @HiltViewModel class CustomViewModel @Inject constructor( @Assisted private val handle: SavedStateHandle, private val service: NetworkService ) : ViewModel()实测发现:当使用导航参数时,Hilt会自动将参数存入SavedStateHandle,可通过get<String>("id")获取。
4. 高级技巧与性能优化
4.1 多模块下的注入策略
对于多模块项目,建议采用:
- 核心模块定义接口:
// core模块 interface AnalyticsService { fun track(event: String) }- 实现模块提供实例:
// analytics模块 @Module @InstallIn(SingletonComponent::class) abstract class AnalyticsModule { @Binds abstract fun bindAnalytics(impl: AnalyticsImpl): AnalyticsService }- ViewModel直接依赖接口:
@HiltViewModel class StatsViewModel @Inject constructor( private val analytics: AnalyticsService ) : ViewModel()4.2 测试替身替换方案
单元测试中替换依赖的完整流程:
- 定义测试模块:
@Module @TestInstallIn( components = [SingletonComponent::class], replaces = [AnalyticsModule::class] ) object FakeAnalyticsModule { @Provides fun provideAnalytics(): AnalyticsService = MockAnalytics() }- 测试类配置:
@HiltAndroidTest class StatsViewModelTest { @get:Rule val hiltRule = HiltAndroidRule(this) @Inject lateinit var viewModel: StatsViewModel @Before fun setup() { hiltRule.inject() } }性能优化点:使用@UninstallModules可以完全移除生产环境模块,减少测试时的依赖加载时间。
5. 实战案例:用户管理系统
5.1 完整依赖图示例
SingletonComponent ├── UserRepository (接口) └── DatabaseModule ViewModelComponent └── UserViewModel ├── 依赖UserRepository └── 依赖SavedStateHandle5.2 核心代码实现
- 数据层模块:
@Module @InstallIn(SingletonComponent::class) interface DatabaseModule { @Binds fun bindUserRepo(impl: UserRepoImpl): UserRepository @Provides fun provideDatabase(): UserDatabase = Room.databaseBuilder(...).build() }- ViewModel实现:
@HiltViewModel class UserViewModel @Inject constructor( private val repo: UserRepository, private val savedStateHandle: SavedStateHandle ) : ViewModel() { private val userId: String = savedStateHandle["userId"]!! val userFlow = repo.getUser(userId).stateIn( scope = viewModelScope, started = SharingStarted.WhileSubscribed(5000), initialValue = null ) }- Compose界面集成:
@Composable fun UserProfileScreen() { val vm: UserViewModel = hiltViewModel() val user by vm.userFlow.collectAsState() user?.let { Text(text = it.name) // 其他UI组件 } }5.3 典型问题排查
问题现象:注入失败,提示"Missing binding"
排查步骤:
- 检查
@HiltAndroidApp是否应用到Application - 确认依赖模块已正确
@InstallIn - 使用
./gradlew kspDebugKotlin --info查看代码生成日志 - 检查混淆规则是否保留了Hilt生成类
解决方案:添加缺失的@Provides方法或@Binds抽象声明
6. 与其他Jetpack组件的协同
6.1 与Navigation的深度集成
通过hiltViewModel()的自动参数传递特性,可以实现:
composable( "user/{userId}/posts/{postId}", arguments = listOf( navArgument("userId") { type = NavType.StringType }, navArgument("postId") { type = NavType.IntType } ) ) { backStackEntry -> val parentVm: UserViewModel = hiltViewModel( backStackEntry.arguments?.getString("userId")?.let { createViewModelStoreOwner(backStackEntry) } ) val postVm: PostViewModel = hiltViewModel() }6.2 与Paging3的配合模式
优化后的分页数据加载方案:
@HiltViewModel class PagingViewModel @Inject constructor( private val pagingSource: UserPagingSource ) : ViewModel() { val pagingFlow = Pager( config = PagingConfig(pageSize = 20), initialKey = 0 ) { pagingSource }.flow.cachedIn(viewModelScope) }对应的Compose消费:
@Composable fun UserList() { val vm: PagingViewModel = hiltViewModel() val lazyPagingItems = vm.pagingFlow.collectAsLazyPagingItems() LazyColumn { items(lazyPagingItems) { user -> UserItem(user) } } }7. 版本适配与兼容方案
7.1 各版本关键变化
| Hilt版本 | Compose适配要点 |
|---|---|
| 2.44 | 初始兼容版本,需手动添加导航扩展 |
| 2.45 | 引入自动SavedStateHandle注入 |
| 2.46 | 优化多模块编译性能 |
| 2.47 | 支持KSP代码生成 |
| 2.48 | 默认集成Navigation支持 |
7.2 降级兼容策略
对于必须使用旧版本的项目,可采用桥接方案:
@Composable fun legacyHiltViewModelCreator(): ViewModelProvider.Factory { val application = LocalContext.current.applicationContext as Application return ViewModelProvider.AndroidViewModelFactory(application) } @Composable fun LegacyScreen() { val vm: LegacyViewModel = viewModel( factory = legacyHiltViewModelCreator() ) }8. 架构设计建议
8.1 分层架构实现
推荐的项目结构:
com.example.app ├── di (Hilt模块) ├── data (Repository实现) ├── domain (业务模型) └── presentation ├── viewmodel (所有ViewModel) └── compose (UI层)8.2 状态管理规范
采用"单向数据流+事件分离"模式:
@HiltViewModel class FormViewModel @Inject constructor() : ViewModel() { private val _state = mutableStateOf(FormState()) val state: State<FormState> = _state private val _events = Channel<FormEvent>() val events = _events.receiveAsFlow() fun onAction(action: FormAction) { when(action) { is FormAction.NameChanged -> _state.value = ... FormAction.Submit -> viewModelScope.launch { _events.send(FormEvent.SubmissionSuccess) } } } }在Compose中消费:
@Composable fun FormScreen() { val vm: FormViewModel = hiltViewModel() val state by vm.state LaunchedEffect(Unit) { vm.events.collect { event -> when(event) { is FormEvent.SubmissionSuccess -> showToast() } } } }这种模式下,Hilt负责依赖注入,ViewModel处理业务逻辑,Composable只关注UI渲染和事件转发。