Hilt 依赖注入:从手动 new 到自动装配

21 阅读8分钟

Hilt 依赖注入:从手动 new 到自动装配

什么是依赖注入

在写代码时,一个类经常需要用到其他类。比如 ArticleViewModel 需要 ArticleRepositoryArticleRepository 又需要 ArticleDaoApiService

最直接的写法是在构造函数里自己创建:

class ArticleViewModel {
    private val repository = ArticleRepository(
        ArticleDao(database),
        ApiService.create()
    )
}

这种做法有几个问题:

  1. 耦合度高。 ViewModel 直接知道怎么创建 Repository、Dao、ApiService,换实现要改 ViewModel 代码。
  2. 难以测试。 写单元测试时想传一个假数据库或 Mock 接口进去,发现做不到。
  3. 对象复用困难。 多个地方都需要 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 的核心优势:

  1. Android 感知。 自动为 Application、Activity、Fragment、Service、ViewModel 提供注入支持。
  2. 编译期验证。 依赖缺失或类型不匹配会在编译时暴露,不会等到运行时崩溃。
  3. 标准化。 所有组件使用相同的注解体系,团队协作成本低。
  4. 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 时,它会自动去找 ApiServiceArticleDao 的创建方式,层层解析,直到所有依赖都满足。

如果某个依赖找不到,编译时就会报错,而不是等到运行时崩溃。这是 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 中不同的组件有不同的作用域:

作用域组件生命周期
@SingletonSingletonComponent整个应用
@ActivityScopedActivityComponentActivity 存活期间
@FragmentScopedFragmentComponentFragment 存活期间
@ViewModelScopedViewModelComponentViewModel 存活期间
@ServiceScopedServiceComponentService 存活期间

一般规则:

  • 网络层、数据库、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

每个文件只做一件事,依赖关系清晰:

  1. NetworkModule 提供 ApiService
  2. DatabaseModule 提供 ArticleDao
  3. RepositoryModuleArticleRepositoryImpl 绑定到 ArticleRepository 接口。
  4. ArticleViewModel 构造函数注入 ArticleRepository
  5. 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 的核心用法可以归纳为:

  1. @HiltAndroidApp 放在 Application 上,启动 Hilt。
  2. @AndroidEntryPoint 放在需要注入的 Android 组件上。
  3. @Inject constructor 标记普通类的构造函数。
  4. @Module + @InstallIn + @Provides 提供不能直接构造的对象。
  5. @Binds 简洁地绑定接口和实现。
  6. @HiltViewModel 配合 by viewModels()hiltViewModel() 注入 ViewModel。
  7. @Singleton 控制全局单例,其他作用域按需使用。
  8. @Qualifier 区分同类型的多个实现。

依赖注入不是为了让代码"看起来高级",而是为了解耦、方便测试、统一对象管理。Hilt 把这件事的门槛降到了很低,大部分 Android 项目都可以直接用起来。

下一篇会聊 Paging 3 分页加载,它和 Hilt、ViewModel、Flow 配合后,列表页的数据加载会变得非常清晰。