Hilt 依赖注入:从手动 new 到自动装配
什么是依赖注入
在写代码时,一个类经常需要用到其他类。比如 ArticleViewModel 需要 ArticleRepository,ArticleRepository 又需要 ArticleDao 和 ApiService。
最直接的写法是在构造函数里自己创建:
class ArticleViewModel {
private val repository = ArticleRepository(
ArticleDao(database),
ApiService.create()
)
}
这种做法有几个问题:
- 耦合度高。 ViewModel 直接知道怎么创建 Repository、Dao、ApiService,换实现要改 ViewModel 代码。
- 难以测试。 写单元测试时想传一个假数据库或 Mock 接口进去,发现做不到。
- 对象复用困难。 多个地方都需要
ApiService,如果各自创建,就不是同一个实例。
依赖注入的思路很简单:不要让类自己创建依赖对象,而是从外部传进来。
class ArticleViewModel(
private val repository: ArticleRepository
) {
// 直接用 repository,不关心它怎么来的
}
但手写依赖注入时,需要在 Application 或 Activity 里手动层层构造对象,代码量会快速膨胀。这就是 DI 框架的价值——帮你自动完成对象的创建和传递。
为什么选 Hilt
Android 上常见的 DI 方案有 Dagger、Koin 和 Hilt。
- Dagger 功能强大但配置复杂,学习曲线陡峭。
- Koin 基于 Kotlin DSL,写法简洁,但运行时解析,编译期不检查。
- Hilt 是 Google 在 Dagger 基础上封装的 Android 专用方案。它保留了编译期类型检查,同时大幅简化了配置。
Hilt 的核心优势:
- Android 感知。 自动为 Application、Activity、Fragment、Service、ViewModel 提供注入支持。
- 编译期验证。 依赖缺失或类型不匹配会在编译时暴露,不会等到运行时崩溃。
- 标准化。 所有组件使用相同的注解体系,团队协作成本低。
- Google 官方推荐。 Jetpack 组件(如 WorkManager、Navigation)逐步提供 Hilt 集成。
接入 Hilt
添加依赖
在项目根 build.gradle 中:
plugins {
id 'com.google.dagger.hilt.android' version '2.51' apply false
}
在 app 模块 build.gradle 中:
plugins {
id 'com.android.application'
id 'kotlin-kapt'
id 'com.google.dagger.hilt.android'
}
dependencies {
implementation "com.google.dagger:hilt-android:2.51"
kapt "com.google.dagger:hilt-android-compiler:2.51"
}
如果项目使用 KSP 替代 kapt:
plugins {
id 'com.google.devtools.ksp'
id 'com.google.dagger.hilt.android'
}
dependencies {
implementation "com.google.dagger:hilt-android:2.51"
ksp "com.google.dagger:hilt-android-compiler:2.51"
}
Application 入口
Hilt 需要一个继承自 Application 的类,并标注 @HiltAndroidApp:
@HiltAndroidApp
class MyApplication : Application()
别忘了在 AndroidManifest.xml 中声明:
<application
android:name=".MyApplication"
... >
@HiltAndroidApp 会触发 Hilt 的代码生成,创建一个应用级别的依赖容器。这是整个 Hilt 配置里唯一需要手动改 Application 的地方。
@AndroidEntryPoint:让 Android 组件支持注入
Activity、Fragment、Service、BroadcastReceiver 需要用 @AndroidEntryPoint 标记后才能使用注入:
@AndroidEntryPoint
class ArticleActivity : AppCompatActivity() {
@Inject
lateinit var repository: ArticleRepository
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// repository 已经可以使用了
}
}
Fragment 也一样:
@AndroidEntryPoint
class ArticleFragment : Fragment() {
@Inject
lateinit var repository: ArticleRepository
}
@AndroidEntryPoint 的含义是:这个 Android 组件需要 Hilt 提供依赖。Hilt 会在合适的生命周期回调中完成注入。
注意:如果 Fragment 的宿主 Activity 没有标 @AndroidEntryPoint,Fragment 的注入会报错。Hilt 的依赖关系是沿着组件层级向上传递的。
@Inject:标记构造函数注入
对于普通的 Kotlin 类,用 @Inject 标记构造函数即可让 Hilt 知道如何创建它:
class ArticleRepository @Inject constructor(
private val apiService: ApiService,
private val articleDao: ArticleDao
) {
suspend fun getArticles(): List<Article> {
val remote = apiService.fetchArticles()
articleDao.insertAll(remote.map { it.toEntity() })
return remote
}
}
当 Hilt 需要创建 ArticleRepository 时,它会自动去找 ApiService 和 ArticleDao 的创建方式,层层解析,直到所有依赖都满足。
如果某个依赖找不到,编译时就会报错,而不是等到运行时崩溃。这是 Hilt(基于 Dagger)最实用的特性之一。
@Module 和 @Provides:告诉 Hilt 如何创建特殊对象
不是所有类都能用 @Inject constructor 标记。比如第三方库的类、接口实现、需要复杂初始化逻辑的对象。这时需要用 Module 来提供。
@Module
@InstallIn(SingletonComponent::class)
object NetworkModule {
@Provides
@Singleton
fun provideOkHttpClient(): OkHttpClient {
return OkHttpClient.Builder()
.connectTimeout(30, TimeUnit.SECONDS)
.addInterceptor(HttpLoggingInterceptor().apply {
level = HttpLoggingInterceptor.Level.BODY
})
.build()
}
@Provides
@Singleton
fun provideRetrofit(client: OkHttpClient): Retrofit {
return Retrofit.Builder()
.baseUrl("https://api.example.com/")
.client(client)
.addConverterFactory(GsonConverterFactory.create())
.build()
}
@Provides
@Singleton
fun provideApiService(retrofit: Retrofit): ApiService {
return retrofit.create(ApiService::class.java)
}
}
几个关键点:
@Module表示这是一个提供依赖的模块。@InstallIn(SingletonComponent::class)表示这个 Module 安装在全局单例容器中,整个应用共享。@Provides标注的方法就是创建对象的方式。@Singleton表示全局只创建一次,所有注入的地方共享同一个实例。
方法的参数就是依赖。Hilt 会自动解析:provideRetrofit 需要 OkHttpClient,Hilt 会先调用 provideOkHttpClient(),拿到结果后传给 provideRetrofit()。
@Binds:接口绑定的简洁写法
如果接口只有一个实现,可以用 @Binds 代替 @Provides,代码更简洁:
@Module
@InstallIn(SingletonComponent::class)
abstract class RepositoryModule {
@Binds
abstract fun bindArticleRepository(
impl: ArticleRepositoryImpl
): ArticleRepository
}
@Binds 只能用在抽象类和抽象方法上。它告诉 Hilt:当需要 ArticleRepository 接口时,提供 ArticleRepositoryImpl 实现。
使用 @Binds 的前提是实现类的构造函数标了 @Inject:
class ArticleRepositoryImpl @Inject constructor(
private val api: ApiService,
private val dao: ArticleDao
) : ArticleRepository {
// ...
}
Room 数据库的 Module 写法
Room 的 Database 和 DAO 通常也用 Module 提供:
@Module
@InstallIn(SingletonComponent::class)
object DatabaseModule {
@Provides
@Singleton
fun provideDatabase(@ApplicationContext context: Context): AppDatabase {
return Room.databaseBuilder(
context,
AppDatabase::class.java,
"app.db"
).build()
}
@Provides
fun provideArticleDao(database: AppDatabase): ArticleDao {
return database.articleDao()
}
}
@ApplicationContext 是 Hilt 内置的 Qualifier,用来注入 Application 的 Context。如果需要 Activity 的 Context,用 @ActivityContext。
这样配置后,任何类的构造函数里只要写 private val dao: ArticleDao,Hilt 就会自动创建并注入。
Hilt 与 ViewModel
ViewModel 的注入方式稍有不同。不能直接用 @Inject constructor 配合 @AndroidEntryPoint,而是要用 @HiltViewModel:
@HiltViewModel
class ArticleViewModel @Inject constructor(
private val repository: ArticleRepository
) : ViewModel() {
private val _uiState = MutableStateFlow(ArticleUiState())
val uiState: StateFlow<ArticleUiState> = _uiState.asStateFlow()
fun loadArticles() {
viewModelScope.launch {
_uiState.value = _uiState.value.copy(loading = true)
try {
val articles = repository.getArticles()
_uiState.value = ArticleUiState(articles = articles)
} catch (e: Exception) {
_uiState.value = _uiState.value.copy(
loading = false,
errorMessage = "加载失败"
)
}
}
}
}
在 Activity 或 Fragment 中获取 ViewModel 时,不需要任何额外配置:
@AndroidEntryPoint
class ArticleActivity : AppCompatActivity() {
private val viewModel: ArticleViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
viewModel.loadArticles()
}
}
Compose 中使用也很自然:
@Composable
fun ArticleScreen(
viewModel: ArticleViewModel = hiltViewModel()
) {
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
// 根据 uiState 渲染 UI
}
hiltViewModel() 来自 androidx.hilt:hilt-navigation-compose,需要额外添加依赖:
implementation "androidx.hilt:hilt-navigation-compose:1.2.0"
作用域:控制对象的生命周期
Hilt 中不同的组件有不同的作用域:
| 作用域 | 组件 | 生命周期 |
|---|---|---|
@Singleton |
SingletonComponent |
整个应用 |
@ActivityScoped |
ActivityComponent |
Activity 存活期间 |
@FragmentScoped |
FragmentComponent |
Fragment 存活期间 |
@ViewModelScoped |
ViewModelComponent |
ViewModel 存活期间 |
@ServiceScoped |
ServiceComponent |
Service 存活期间 |
一般规则:
- 网络层、数据库、Repository 用
@Singleton,全局共享。 - ViewModel 用
@HiltViewModel,内部依赖默认跟随 ViewModel 生命周期。 - 只有确实需要和 Activity 或 Fragment 生命周期绑定的对象,才使用对应作用域。
不要在 SingletonComponent 的 Module 中注入 Activity Context。全局单例的寿命远超 Activity,持有 Activity Context 会导致内存泄漏。
@Qualifier:区分同类型依赖
有时接口有多个实现,Hilt 不知道该注入哪一个。比如项目中有正式环境和测试环境两套 ApiService:
@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class ProdApi
@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class TestApi
在 Module 中标注:
@Module
@InstallIn(SingletonComponent::class)
object NetworkModule {
@Provides
@Singleton
@ProdApi
fun provideProdApi(retrofit: Retrofit): ApiService {
return retrofit.create(ProdApiService::class.java)
}
@Provides
@Singleton
@TestApi
fun provideTestApi(retrofit: Retrofit): ApiService {
return retrofit.create(TestApiService::class.java)
}
}
注入时指定要哪个:
class DataRepository @Inject constructor(
@ProdApi private val apiService: ApiService
)
Hilt 内置了两个常用 Qualifier:@ApplicationContext 和 @ActivityContext,不需要自己定义。
实战:一个完整的 Hilt 项目结构
整理一下典型项目中 Hilt 的目录组织:
com.example.app/
├── MyApplication.kt // @HiltAndroidApp
├── di/
│ ├── NetworkModule.kt // OkHttpClient, Retrofit, ApiService
│ ├── DatabaseModule.kt // Room Database, DAO
│ └── RepositoryModule.kt // @Binds 接口绑定
├── data/
│ ├── remote/
│ │ └── ApiService.kt
│ ├── local/
│ │ ├── AppDatabase.kt
│ │ └── ArticleDao.kt
│ └── repository/
│ ├── ArticleRepository.kt // 接口
│ └── ArticleRepositoryImpl.kt // @Inject constructor
├── ui/
│ ├── ArticleActivity.kt // @AndroidEntryPoint
│ └── ArticleViewModel.kt // @HiltViewModel
每个文件只做一件事,依赖关系清晰:
NetworkModule提供ApiService。DatabaseModule提供ArticleDao。RepositoryModule把ArticleRepositoryImpl绑定到ArticleRepository接口。ArticleViewModel构造函数注入ArticleRepository。ArticleActivity通过by viewModels()拿到 ViewModel。
整个过程没有任何手动 new 或工厂方法。Hilt 在编译期生成了所有必要的胶水代码。
测试中的 Hilt
Hilt 对测试支持很好。可以针对测试替换某些依赖:
@UninstallModules(NetworkModule::class)
@HiltAndroidTest
class ArticleRepositoryTest {
@Inject
lateinit var repository: ArticleRepository
@Test
fun testGetArticles() = runTest {
val articles = repository.getArticles()
assertTrue(articles.isNotEmpty())
}
}
也可以用 @TestInstallIn 在测试时替换整个 Module:
@Module
@TestInstallIn(
components = [SingletonComponent::class],
replaces = [NetworkModule::class]
)
object FakeNetworkModule {
@Provides
@Singleton
fun provideApiService(): ApiService {
return FakeApiService()
}
}
这样测试时用的是假接口实现,不需要真实网络请求。
常见坑
忘记加 @AndroidEntryPoint。 Activity 或 Fragment 里用 @Inject 之前必须先标这个注解,否则注入不会生效,变量为 null。
Module 忘记 @InstallIn。 @Module 必须配合 @InstallIn 指定安装到哪个组件,否则编译报错。
在 SingletonComponent 中使用 Activity Context。 全局单例的生命周期远超 Activity,持有 Activity Context 会泄漏。需要 Context 时用 @ApplicationContext。
ViewModel 没有用 @HiltViewModel。 直接在 ViewModel 构造函数写 @Inject 配合 by viewModels() 不行。ViewModel 必须用 @HiltViewModel 标注。
循环依赖。 A 依赖 B,B 依赖 A,Hilt 在编译期会检测到并报错。遇到时需要重新审视架构设计,通常意味着职责划分有问题。
第三方库对象忘记提供 Module。 像 Gson、Picasso、Firebase 这些第三方库的实例不能 @Inject constructor,必须通过 @Module + @Provides 提供。
kapt/ksp 增量编译问题。 偶尔遇到编译不通过但代码没问题的情况,Clean + Rebuild 通常能解决。
总结
Hilt 的核心用法可以归纳为:
@HiltAndroidApp放在 Application 上,启动 Hilt。@AndroidEntryPoint放在需要注入的 Android 组件上。@Inject constructor标记普通类的构造函数。@Module+@InstallIn+@Provides提供不能直接构造的对象。@Binds简洁地绑定接口和实现。@HiltViewModel配合by viewModels()或hiltViewModel()注入 ViewModel。@Singleton控制全局单例,其他作用域按需使用。@Qualifier区分同类型的多个实现。
依赖注入不是为了让代码"看起来高级",而是为了解耦、方便测试、统一对象管理。Hilt 把这件事的门槛降到了很低,大部分 Android 项目都可以直接用起来。
下一篇会聊 Paging 3 分页加载,它和 Hilt、ViewModel、Flow 配合后,列表页的数据加载会变得非常清晰。