16、依赖注入:Hilt 集成 Compose,hiltViewModel() 的使用,手动 DI 方案
依赖注入(DI)这个话题,说实话,刚入行那会儿我觉得挺玄乎的。不就是 new 个对象嘛,搞那么复杂干嘛?直到我接手了一个中等规模的项目,ViewModel 里依赖了三四个 Repository,每个 Repository 又依赖了数据源和网络服务……手动 new 来 new 去,改一个构造参数,满屏报红。嗯,从那以后,我再也不敢小看 DI 了。
在 Jetpack Compose 的世界里,依赖注入更是如鱼得水。Compose 的声明式 UI 和 Hilt 的自动注入配合起来,写代码就像搭积木一样顺畅。今天我们就来聊聊 Hilt 怎么和 Compose 集成,以及如果不想用 Hilt,手动 DI 该怎么搞。
16.1 Hilt 是什么?为什么选它?
Hilt 是 Google 官方基于 Dagger 打造的 Android 依赖注入库。说白了,它就是帮你自动生成那些「创建对象、管理生命周期、注入依赖」的模板代码。我个人习惯用 Hilt 的原因很简单:它和 Jetpack 全家桶的集成度最高,尤其是 ViewModel 和 Navigation。
你想想看,如果没有 DI,一个典型的 ViewModel 可能是这样的:
class MyViewModel : ViewModel() {
private val repository = UserRepository(
apiService = ApiService(),
localDb = LocalDatabase()
)
private val analytics = AnalyticsManager()
}
这代码有什么问题?问题大了。MyViewModel 直接依赖了 ApiService、LocalDatabase 的具体实现。以后想换网络库、换数据库,或者做单元测试,都得改 ViewModel 的代码。耦合度太高,维护起来很痛苦。
用了 Hilt 之后,代码变成这样:
@HiltViewModel
class MyViewModel @Inject constructor(
private val repository: UserRepository,
private val analytics: AnalyticsManager
) : ViewModel() {
// 业务逻辑
}
清爽多了,对吧?ViewModel 不再关心依赖是怎么创建的,它只管用。这就是 DI 的核心思想:控制反转。
16.2 Hilt 集成 Compose 的步骤
要在 Compose 项目里用 Hilt,其实就三步。我在项目中踩过不少坑,这里给你梳理清楚了。
16.2.1 添加依赖
首先,在项目级的 build.gradle 里加上 Hilt 插件:
// 项目级 build.gradle
plugins {
id 'com.google.dagger.hilt.android' version '2.48' apply false
}
然后在 app 模块的 build.gradle 里:
// app 模块 build.gradle
plugins {
id 'kotlin-kapt'
id 'com.google.dagger.hilt.android'
}
android {
// ... 其他配置
}
dependencies {
implementation 'com.google.dagger:hilt-android:2.48'
kapt 'com.google.dagger:hilt-compiler:2.48'
implementation 'androidx.hilt:hilt-navigation-compose:1.1.0'
}
16.2.2 创建 Application 类
每个使用 Hilt 的项目都需要一个带有 @HiltAndroidApp 注解的 Application 类:
@HiltAndroidApp
class MyApplication : Application() {
override fun onCreate() {
super.onCreate()
// 初始化工作
}
}
别忘了在 AndroidManifest.xml 里声明这个 Application:
<application
android:name=".MyApplication"
...>
...
</application>
16.2.3 在 Activity 中启用 Hilt
Activity 也需要加上 @AndroidEntryPoint 注解:
@AndroidEntryPoint
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
MyAppTheme {
MyScreen()
}
}
}
}
到这里,Hilt 的基础环境就搭好了。接下来我们看看最核心的部分——在 Compose 里怎么拿到 ViewModel。
16.3 hiltViewModel() 的使用
在 Compose 中获取 ViewModel,传统的方式是 viewModel(),但它只能拿到普通的 ViewModel。如果你想让 ViewModel 自动注入依赖,就得用 hiltViewModel()。
用法非常简单:
@Composable
fun MyScreen() {
val viewModel: MyViewModel = hiltViewModel()
// 使用 viewModel 的状态
val uiState by viewModel.uiState.collectAsState()
Column {
Text(text = uiState.userName)
Button(onClick = { viewModel.refresh() }) {
Text("刷新")
}
}
}
就这么一行代码,Hilt 就会自动帮你创建 MyViewModel,并把它的所有依赖(比如 UserRepository、AnalyticsManager)都注入进去。你完全不需要手动管理 ViewModel 的生命周期,Compose 和 Hilt 会帮你处理好。
16.4 手动 DI 方案
Hilt 虽然好用,但有时候项目比较小,或者你不想引入那么多编译期注解处理,手动 DI 也是一个不错的选择。说白了,手动 DI 就是自己写一个「对象工厂」,需要什么就从这个工厂里拿。
我给大家展示一个简单的手动 DI 实现:
// 定义一个依赖容器
class AppContainer(private val context: Context) {
// 单例的数据库
private val database by lazy { AppDatabase.getInstance(context) }
// 单例的网络服务
private val apiService by lazy { Retrofit.Builder()
.baseUrl("https://api.example.com")
.build()
.create(ApiService::class.java)
}
// Repository 的工厂方法
fun createUserRepository(): UserRepository {
return UserRepository(apiService, database.userDao())
}
// ViewModel 的工厂方法
fun createUserViewModel(): UserViewModel {
return UserViewModel(createUserRepository())
}
}
然后在 Application 里持有这个容器:
class MyApplication : Application() {
lateinit var appContainer: AppContainer
private set
override fun onCreate() {
super.onCreate()
appContainer = AppContainer(this)
}
}
在 Composable 中使用时,通过 LocalContext 拿到 Application,再拿到容器:
@Composable
fun MyScreen() {
val context = LocalContext.current
val appContainer = (context.applicationContext as MyApplication).appContainer
// 手动创建 ViewModel
val viewModel = remember { appContainer.createUserViewModel() }
// 使用 viewModel
val uiState by viewModel.uiState.collectAsState()
// ... UI 代码
}
手动 DI 的好处是简单直接,没有注解,没有编译期代码生成,调试起来也方便。但缺点也很明显:项目大了之后,依赖关系会变得复杂,手动管理容易出错。你想想看,如果有几十个 ViewModel,每个 ViewModel 依赖三四个 Repository,手动写工厂方法得写到什么时候?
16.5 两种方案的对比
| 对比维度 | Hilt | 手动 DI |
|---|---|---|
| 学习成本 | 中等,需要理解注解和组件 | 低,只需要理解对象创建 |
| 代码量 | 少,注解自动生成 | 多,需要手动写工厂 |
| 可维护性 | 高,依赖关系清晰 | 中等,项目大了容易乱 |
| 编译速度 | 较慢,有注解处理 | 快,没有额外处理 |
| 测试友好度 | 高,可以轻松替换依赖 | 中等,需要手动 mock |
| 适用场景 | 中大型项目 | 小型项目或学习 |
我个人建议:如果是正式项目,直接用 Hilt。虽然刚开始配置有点麻烦,但长期来看收益很大。如果是自己写着玩的小 Demo,手动 DI 完全够用,还能帮你理解依赖注入的本质。
16.6 避坑指南
最后分享几个我踩过的坑,希望能帮你少走弯路:
- @HiltViewModel 和 @AndroidEntryPoint 不能少:ViewModel 类上要加 @HiltViewModel,Activity 或 Fragment 上要加 @AndroidEntryPoint。少一个,hiltViewModel() 就会报错。
- 构造参数必须用 @Inject constructor:Hilt 是通过构造函数注入的,所以 ViewModel 的构造函数必须加上 @Inject 注解。
- Navigation 中的 ViewModel 作用域:如果你在 Navigation 中使用 hiltViewModel(),默认是跟着 NavBackStackEntry 的生命周期走的。如果想在多个目的地之间共享 ViewModel,需要指定 navGraphViewModels。
- 手动 DI 别忘了 remember:在 Composable 中手动创建 ViewModel 时,一定要用 remember 包裹,否则每次重组都会创建新的实例,导致状态丢失。
依赖注入这件事,说白了就是「把创建对象的权力交出去」。不管是 Hilt 还是手动 DI,核心思想都一样。理解了这一点,你就能在各种 DI 方案之间游刃有余。
- Hilt 是 Google 官方推荐的 Android DI 方案,与 Compose 集成度最高
- hiltViewModel() 是 Compose 中获取 Hilt 注入 ViewModel 的标准方式
- 手动 DI 适合小型项目,能帮助你理解依赖注入的本质
- 选择哪种方案取决于项目规模和团队习惯,没有绝对的好坏