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

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

什么是依赖注入

在写代码时,一个类经常需要用到其他类。比如 ArticleViewModel 需要 ArticleRepository,ArticleRepository 又需要 ArticleDao 和 ApiService。

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

kotlin 复制代码
class ArticleViewModel {
    private val repository = ArticleRepository(
        ArticleDao(database),
        ApiService.create()
    )
}

这种做法有几个问题:

  1. 耦合度高。 ViewModel 直接知道怎么创建 Repository、Dao、ApiService,换实现要改 ViewModel 代码。
  2. 难以测试。 写单元测试时想传一个假数据库或 Mock 接口进去,发现做不到。
  3. 对象复用困难。 多个地方都需要 ApiService,如果各自创建,就不是同一个实例。

依赖注入的思路很简单:不要让类自己创建依赖对象,而是从外部传进来。

kotlin 复制代码
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 中:

groovy 复制代码
plugins {
    id 'com.google.dagger.hilt.android' version '2.51' apply false
}

在 app 模块 build.gradle 中:

groovy 复制代码
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:

groovy 复制代码
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:

kotlin 复制代码
@HiltAndroidApp
class MyApplication : Application()

别忘了在 AndroidManifest.xml 中声明:

xml 复制代码
<application
    android:name=".MyApplication"
    ... >

@HiltAndroidApp 会触发 Hilt 的代码生成,创建一个应用级别的依赖容器。这是整个 Hilt 配置里唯一需要手动改 Application 的地方。

@AndroidEntryPoint:让 Android 组件支持注入

Activity、Fragment、Service、BroadcastReceiver 需要用 @AndroidEntryPoint 标记后才能使用注入:

kotlin 复制代码
@AndroidEntryPoint
class ArticleActivity : AppCompatActivity() {

    @Inject
    lateinit var repository: ArticleRepository

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        // repository 已经可以使用了
    }
}

Fragment 也一样:

kotlin 复制代码
@AndroidEntryPoint
class ArticleFragment : Fragment() {

    @Inject
    lateinit var repository: ArticleRepository
}

@AndroidEntryPoint 的含义是:这个 Android 组件需要 Hilt 提供依赖。Hilt 会在合适的生命周期回调中完成注入。

注意:如果 Fragment 的宿主 Activity 没有标 @AndroidEntryPoint,Fragment 的注入会报错。Hilt 的依赖关系是沿着组件层级向上传递的。

@Inject:标记构造函数注入

对于普通的 Kotlin 类,用 @Inject 标记构造函数即可让 Hilt 知道如何创建它:

kotlin 复制代码
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 来提供。

kotlin 复制代码
@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,代码更简洁:

kotlin 复制代码
@Module
@InstallIn(SingletonComponent::class)
abstract class RepositoryModule {

    @Binds
    abstract fun bindArticleRepository(
        impl: ArticleRepositoryImpl
    ): ArticleRepository
}

@Binds 只能用在抽象类和抽象方法上。它告诉 Hilt:当需要 ArticleRepository 接口时,提供 ArticleRepositoryImpl 实现。

使用 @Binds 的前提是实现类的构造函数标了 @Inject:

kotlin 复制代码
class ArticleRepositoryImpl @Inject constructor(
    private val api: ApiService,
    private val dao: ArticleDao
) : ArticleRepository {
    // ...
}

Room 数据库的 Module 写法

Room 的 Database 和 DAO 通常也用 Module 提供:

kotlin 复制代码
@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:

kotlin 复制代码
@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 时,不需要任何额外配置:

kotlin 复制代码
@AndroidEntryPoint
class ArticleActivity : AppCompatActivity() {

    private val viewModel: ArticleViewModel by viewModels()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        viewModel.loadArticles()
    }
}

Compose 中使用也很自然:

kotlin 复制代码
@Composable
fun ArticleScreen(
    viewModel: ArticleViewModel = hiltViewModel()
) {
    val uiState by viewModel.uiState.collectAsStateWithLifecycle()
    // 根据 uiState 渲染 UI
}

hiltViewModel() 来自 androidx.hilt:hilt-navigation-compose,需要额外添加依赖:

groovy 复制代码
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:

kotlin 复制代码
@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class ProdApi

@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class TestApi

在 Module 中标注:

kotlin 复制代码
@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)
    }
}

注入时指定要哪个:

kotlin 复制代码
class DataRepository @Inject constructor(
    @ProdApi private val apiService: ApiService
)

Hilt 内置了两个常用 Qualifier:@ApplicationContext 和 @ActivityContext,不需要自己定义。

实战:一个完整的 Hilt 项目结构

整理一下典型项目中 Hilt 的目录组织:

kotlin 复制代码
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. RepositoryModule 把 ArticleRepositoryImpl 绑定到 ArticleRepository 接口。
  4. ArticleViewModel 构造函数注入 ArticleRepository。
  5. ArticleActivity 通过 by viewModels() 拿到 ViewModel。

整个过程没有任何手动 new 或工厂方法。Hilt 在编译期生成了所有必要的胶水代码。

测试中的 Hilt

Hilt 对测试支持很好。可以针对测试替换某些依赖:

kotlin 复制代码
@UninstallModules(NetworkModule::class)
@HiltAndroidTest
class ArticleRepositoryTest {

    @Inject
    lateinit var repository: ArticleRepository

    @Test
    fun testGetArticles() = runTest {
        val articles = repository.getArticles()
        assertTrue(articles.isNotEmpty())
    }
}

也可以用 @TestInstallIn 在测试时替换整个 Module:

kotlin 复制代码
@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 配合后,列表页的数据加载会变得非常清晰。

相关推荐
码事漫谈3 小时前
五道题,测测你有没有真的入门 AI
后端
蜗牛互联网3 小时前
Python + JSON Schema 实现工单结构化输出与本地复核
java·人工智能·后端
大侠归来5 小时前
Spring Boot 2.1 → 3.5 迁移推演:从实战出发的完整路线图
java·spring boot·后端
海马6 小时前
Spring Boot 开发知识整理
java·spring boot·后端
谢亮_vipxieliang6 小时前
Spring Boot 3.x 从零开始——环境搭建与第一个 REST 项目
java·spring boot·后端
念何架构之路6 小时前
go-grpc核心抽象接口
开发语言·后端·golang
FYKJ_20106 小时前
django个性化新闻推荐35173-计算机课程设计、毕业设计
java·spring boot·后端·python·架构·django·课程设计
夜之眷属6 小时前
服务启动即崩:InetAddress.getLocalHost() 抛 UnknownHostException 排查记
java·运维·后端·docker
金銀銅鐵7 小时前
[Java] 用GUI展示class文件顶层的 access_flags
后端·python·ai编程