Administrator
发布于 2026-08-25 / 26 阅读
0
0

系列文章《Android 工程师的 100 篇进阶笔记》第 8 篇:Hilt + 依赖注入:从入门到架构层设计

几乎所有 Android 项目都会遇到一个共同问题:对象该在哪里创建?

一开始我们用单例:Repository.getInstance()。然后发现测试困难、生命周期混乱、Mock 麻烦。再后来项目变大,模块之间互相引用,改动一处可能引发连锁反应。

依赖注入(DI)就是解决这个问题的方法论,而 Hilt 是 Google 在 Android 上推荐的 DI 框架。这篇会讲清楚:

  1. DI 到底解决什么问题。

  2. Hilt 的核心 API 和生命周期绑定。

  3. 模块化、多进程架构下如何设计 DI 边界。


一、没有 DI 时,我们在踩什么坑?

1.1 手动单例的隐患

object UserRepository {
    private val api = RetrofitClient.api
    private val db = AppDatabase.getInstance()

    suspend fun getUser(id: String) = api.getUser(id)
}

看起来没问题,但:

  • 单元测试时很难把 api 替换成 Mock。

  • RetrofitClient 和 AppDatabase 的初始化顺序耦合。

  • 如果 UserRepository 需要 Context,只能从全局拿。

1.2 依赖隐藏起来,测试就痛苦

class LoginViewModel : ViewModel() {
    private val repo = UserRepository
}

这个 ViewModel 直接依赖全局单例,测试时无法注入假数据。

DI 的核心思想:对象不自己创建依赖,而是由外部注入。


二、Hilt 入门:五个注解走天下

Hilt 是 Dagger 的 Android 封装,保留了 Dagger 的编译时校验,同时大幅简化了 API。

2.1 五个核心注解

注解

作用

@HiltAndroidApp

标记 Application,触发 Hilt 代码生成

@AndroidEntryPoint

标记 Activity / Fragment / Service / ViewModel 等入口点

@Module + @InstallIn

定义如何提供依赖

@Provides

在 Module 里提供对象实例

@Inject

构造函数或字段注入

@Singleton

生命周期为应用全局

2.2 一个最小示例

Application:

@HiltAndroidApp
class MyApp : Application()

Module:

@Module
@InstallIn(SingletonComponent::class)
object NetworkModule {

    @Provides
    @Singleton
    fun provideRetrofit(): Retrofit {
        return Retrofit.Builder()
            .baseUrl("https://api.example.com/")
            .addConverterFactory(GsonConverterFactory.create())
            .build()
    }

    @Provides
    @Singleton
    fun provideApiService(retrofit: Retrofit): ApiService {
        return retrofit.create(ApiService::class.java)
    }
}

Repository:

class UserRepository @Inject constructor(
    private val api: ApiService,
    private val dao: UserDao
)

ViewModel:

@HiltViewModel
class LoginViewModel @Inject constructor(
    private val repository: UserRepository
) : ViewModel() {
    // ...
}

Activity:

@AndroidEntryPoint
class LoginActivity : AppCompatActivity() {
    private val viewModel: LoginViewModel by viewModels()
}

三、生命周期绑定:Hilt 的组件层级

Hilt 内置了多个生命周期组件,Module 安装到不同组件里,提供的对象生命周期就不同。

Hilt 组件

对应 Android 生命周期

SingletonComponent

Application

ActivityComponent

Activity

FragmentComponent

Fragment

ViewModelComponent

ViewModel

ServiceComponent

Service

3.1 不要全局 Singleton 一把梭

很多新手会把所有对象都标成 @Singleton,这是浪费内存。

@Module
@InstallIn(SingletonComponent::class)
object AppModule {

    @Provides
    @Singleton
    fun provideLoginUseCase() = LoginUseCase()  // 只在登录页用,不该全局
}

正确做法:根据真实生命周期选择组件。

@Module
@InstallIn(ActivityComponent::class)
object LoginModule {
    @Provides
    fun provideLoginUseCase() = LoginUseCase()
}

3.2 ViewModelScoped:ViewModel 存活期间复用

@Module
@InstallIn(ViewModelComponent::class)
object DetailModule {
    @Provides
    @ViewModelScoped
    fun provideDetailUseCase() = DetailUseCase()
}

ViewModel 旋转屏幕后依然存活,DetailUseCase 不会重复创建。


四、限定符:解决同一类型的不同实现

当一个类型有多个实现时,用 @Qualifier 区分。

4.1 示例:线上 API 和 Mock API

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

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

@Module
@InstallIn(SingletonComponent::class)
object ApiModule {

    @ProdApi
    @Provides
    @Singleton
    fun provideProdApi(): ApiService = Retrofit.Builder()...

    @MockApi
    @Provides
    @Singleton
    fun provideMockApi(): ApiService = MockApiService()
}

使用:

class UserRepository @Inject constructor(
    @ProdApi private val api: ApiService
)

五、模块化架构下的 DI 设计

大型项目会拆成多个 Gradle Module:

  • :app

  • :feature:login

  • :feature:home

  • :core:network

  • :core:data

  • :core:common

5.1 依赖方向必须清晰

:app
├── :feature:login
├── :feature:home
├── :core:data
│   ├── :core:network
│   └── :core:database
└── :core:common

DI 模块的安装位置要匹配依赖方向:

  • :core:network 里的 NetworkModule 安装到 SingletonComponent。

  • :feature:login 里的 LoginModule 安装到 ActivityComponent 或 ViewModelComponent。

5.2 避免 Module 之间的循环依赖

如果 :feature:login 需要 :feature:home 的类,同时 :feature:home 也需要 :feature:login 的类,说明模块边界划分错了。

解耦方法:

  • 把公共接口抽到 :core:common。

  • 用 Hilt 的多绑定(@IntoSet、@ElementsIntoSet)做插件化注册。

5.3 Hilt 多绑定:Feature 注册表

interface FeatureEntry {
    val route: String
}

@Module
@InstallIn(SingletonComponent::class)
abstract class FeatureModule {
    @Binds
    @IntoSet
    abstract fun bindHomeEntry(home: HomeFeatureEntry): FeatureEntry

    @Binds
    @IntoSet
    abstract fun bindLoginEntry(login: LoginFeatureEntry): FeatureEntry
}

然后 App 层拿到所有 Feature:

class Navigator @Inject constructor(
    entries: Set<FeatureEntry>
)

这是模块化导航的常见做法。


六、Hilt 的常见踩坑清单

坑

原因

修复

@Inject 字段为 null

没有在类上加 @AndroidEntryPoint

加注解

编译报错找不到 Component

忘记 @HiltAndroidApp

标记 Application

循环依赖

A 依赖 B,B 依赖 A

抽接口或用 Provider 延迟

同一个类型多个实现冲突

没有 Qualifier

用 @Qualifier 区分

所有对象都 @Singleton

生命周期过长

按组件选择作用域


七、DI 选型:Hilt vs Koin vs 手动工厂

框架

编译期校验

学习曲线

适合场景

Hilt

✅ 强

中等

中大型项目

Koin

❌ 运行期

低

小型项目、Kotlin Multiplatform

手动工厂

❌ 无

低

原型、极小项目

建议:Android 原生项目直接上 Hilt。Koin 虽然写起来简单,但运行期才能发现依赖缺失,出问题更难排查。


小结

依赖注入不是“高级语法”,而是管理对象生命周期的工程纪律:

  1. 对象由外部注入,方便替换和测试。

  2. Hilt 用五个注解就能覆盖大部分场景。

  3. 生命周期组件要选准,不要所有对象都 Singleton。

  4. 模块化项目里用 Qualifier 和多绑定解耦。

  5. Hilt 比 Koin 更适合大型 Android 项目。


下篇预告

下一篇《第 9 篇:Kotlin 协程底层:Dispatchers、Job、Flow 原理与异常处理》会把协程讲深一层:

  1. 协程和线程到底是什么关系。

  2. Dispatchers.Default、IO、Main 的区别与实现。

  3. Job、SupervisorJob、CoroutineExceptionHandler 的异常处理链。


参考阅读

  • Hilt 官方文档

  • Dagger 2 官方文档

  • Android 官方应用架构指南

  • 上一篇文章:第 7 篇《Android 跨进程通信:Binder、AIDL、Messenger 与 MMKV 选型》


评论