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'
}
注意:hilt-navigation-compose 这个库是 Compose 和 Hilt 的桥梁,没有它,你在 Composable 函数里就拿不到 Hilt 注入的 ViewModel。我曾经漏掉这个依赖,排查了半天才发现问题。

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 会帮你处理好。

小技巧:如果你在 Navigation 中使用 hiltViewModel(),记得给每个导航目的地设置唯一的 route。否则多个相同类型的 ViewModel 可能会共享同一个实例,导致状态混乱。我在项目中就遇到过这个问题,两个页面共用了同一个 ViewModel 的数据,排查了好久才发现是 route 重复了。

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 适合小型项目,能帮助你理解依赖注入的本质
  • 选择哪种方案取决于项目规模和团队习惯,没有绝对的好坏
依赖注入方案对比与集成流程 Hilt 方案 1. 添加 Hilt 依赖和插件 2. @HiltAndroidApp Application 3. @AndroidEntryPoint Activity 4. hiltViewModel() 获取 ViewModel 自动注入依赖,生命周期管理 手动 DI 方案 1. 创建 AppContainer 容器类 2. 在 Application 中持有容器 3. 手动创建 ViewModel 实例 4. remember 包裹防止重组丢失 简单直接,适合小型项目 VS
公众号:蓝海资料掘金营,微信deep3321