几乎所有 Android 项目都会遇到一个共同问题:对象该在哪里创建?
一开始我们用单例:Repository.getInstance()。然后发现测试困难、生命周期混乱、Mock 麻烦。再后来项目变大,模块之间互相引用,改动一处可能引发连锁反应。
依赖注入(DI)就是解决这个问题的方法论,而 Hilt 是 Google 在 Android 上推荐的 DI 框架。这篇会讲清楚:
DI 到底解决什么问题。
Hilt 的核心 API 和生命周期绑定。
模块化、多进程架构下如何设计 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 五个核心注解
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 安装到不同组件里,提供的对象生命周期就不同。
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 的常见踩坑清单
七、DI 选型:Hilt vs Koin vs 手动工厂
建议:Android 原生项目直接上 Hilt。Koin 虽然写起来简单,但运行期才能发现依赖缺失,出问题更难排查。
小结
依赖注入不是“高级语法”,而是管理对象生命周期的工程纪律:
对象由外部注入,方便替换和测试。
Hilt 用五个注解就能覆盖大部分场景。
生命周期组件要选准,不要所有对象都 Singleton。
模块化项目里用 Qualifier 和多绑定解耦。
Hilt 比 Koin 更适合大型 Android 项目。
下篇预告
下一篇《第 9 篇:Kotlin 协程底层:Dispatchers、Job、Flow 原理与异常处理》会把协程讲深一层:
协程和线程到底是什么关系。
Dispatchers.Default、IO、Main的区别与实现。Job、SupervisorJob、CoroutineExceptionHandler的异常处理链。
参考阅读
Hilt 官方文档
Dagger 2 官方文档
Android 官方应用架构指南
上一篇文章:第 7 篇《Android 跨进程通信:Binder、AIDL、Messenger 与 MMKV 选型》