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,可能有appcorefeature-homefeature-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实例的完整流程:

Hilt ViewModel 注入流程 @HiltViewModel 标注ViewModel类 Hilt 注解处理器 生成HiltViewModelFactory by hiltViewModel() 获取ViewModel实例 Hilt 依赖图(编译时构建) @Inject constructor ViewModel的依赖声明 @Module / @Provides 依赖的提供方式 @Singleton / @Scope 依赖的作用域管理 编译时检查依赖是否完整,运行时直接使用生成的Factory 无需手动编写ViewModelFactory,无需担心生命周期问题 使用

常见问题与避坑

在实际使用中,有几个问题我经常遇到,这里列出来供你参考:

问题 原因 解决方案
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。迁移成本很低,但带来的开发效率提升是实实在在的。