依赖注入(Hilt/Dagger):Hilt基础、@Inject、@Module、@Provides、组件作用域
依赖注入,说白了就是「我要什么,你给我什么」。
我刚开始接触 Android 开发时,习惯直接在 Activity 里 new 对象。后来项目越来越大,改一处代码要牵连好几个文件,那叫一个痛苦。直到我遇到了 Dagger,后来又等来了官方亲儿子 Hilt——嗯,真香。
为什么需要依赖注入?
你想想看,一个类如果自己负责创建所有依赖,它就得知道太多东西了。比如一个网络请求的 ViewModel,它要自己创建 Retrofit、OkHttp、Gson……这哪是业务逻辑,这分明是搬砖。
依赖注入的核心思想就一句话:把创建对象的权力交出去。谁用谁声明,谁创建谁负责。
依赖注入的好处:
- 解耦:类不再依赖具体实现,只依赖接口
- 可测试:轻松替换 Mock 对象
- 可复用:同一个依赖可以在多处共享
- 生命周期管理:作用域控制对象的创建和销毁
Hilt 是什么?
Hilt 是 Google 基于 Dagger 封装的依赖注入框架。它简化了 Dagger 的配置,让你少写很多模板代码。
我个人习惯用 Hilt 替代 Dagger,除非项目对包体积有极致要求。Hilt 的编译期注解处理虽然会多生成一些代码,但开发效率提升是实打实的。
Hilt 基础配置
首先,在项目根 build.gradle 添加 Hilt 插件:
// 根 build.gradle
plugins {
id 'com.google.dagger.hilt.android' version '2.48' apply false
}
然后在 app 模块的 build.gradle 启用:
// app/build.gradle
plugins {
id 'com.android.application'
id 'org.jetbrains.kotlin.android'
id 'com.google.dagger.hilt.android'
id 'kotlin.kapt'
}
android {
// ... 你的配置
}
dependencies {
implementation 'com.google.dagger:hilt-android:2.48'
kapt 'com.google.dagger:hilt-compiler:2.48'
}
最后,在 Application 类上添加 @HiltAndroidApp:
@HiltAndroidApp
class MyApplication : Application() {
override fun onCreate() {
super.onCreate()
// 初始化工作
}
}
小提示:别忘了在 AndroidManifest.xml 中声明你的 Application 类。
@Inject:告诉 Hilt 怎么创建对象
@Inject 有两个用途:
- 标注构造函数:告诉 Hilt「这个类我可以自己创建」
- 标注字段:告诉 Hilt「我需要这个依赖,你给我注入」
看个例子:
// 告诉 Hilt:UserRepository 可以通过构造函数创建
class UserRepository @Inject constructor(
private val apiService: ApiService
) {
fun getUser(): User {
return apiService.fetchUser()
}
}
// 在 Activity 中使用
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
@Inject
lateinit var userRepository: UserRepository
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// userRepository 已经被注入了,直接用
val user = userRepository.getUser()
}
}
这里要注意:@AndroidEntryPoint 是 Hilt 为 Android 组件提供的入口注解。Activity、Fragment、Service、BroadcastReceiver 都需要加这个。
我曾经踩过的坑:在 Fragment 中使用 @Inject 注入时,一定要在 onAttach 之后才能访问注入的字段。如果在构造函数里就访问,会报空指针。因为 Hilt 的注入时机是在 onAttach 之后。
@Module 和 @Provides:当 Hilt 不会自己创建时
有些类不是你能控制的,比如 Retrofit、OkHttp、Room 数据库。这时候就需要 @Module 和 @Provides 出场了。
@Module 标注一个类,告诉 Hilt「这里面有创建依赖的方法」。
@Provides 标注方法,告诉 Hilt「用这个方法创建依赖」。
看代码:
@Module
@InstallIn(SingletonComponent::class)
object NetworkModule {
@Provides
@Singleton
fun provideOkHttpClient(): OkHttpClient {
return OkHttpClient.Builder()
.connectTimeout(30, TimeUnit.SECONDS)
.readTimeout(30, TimeUnit.SECONDS)
.build()
}
@Provides
@Singleton
fun provideRetrofit(okHttpClient: OkHttpClient): Retrofit {
return Retrofit.Builder()
.baseUrl("https://api.example.com/")
.client(okHttpClient)
.addConverterFactory(GsonConverterFactory.create())
.build()
}
@Provides
@Singleton
fun provideApiService(retrofit: Retrofit): ApiService {
return retrofit.create(ApiService::class.java)
}
}
这里有几个关键点:
@InstallIn(SingletonComponent::class):指定这个 Module 安装在哪个组件中。SingletonComponent 表示全局单例。@Singleton:作用域注解,表示这个依赖在整个应用中只有一个实例。- 方法参数会自动注入:比如
provideRetrofit需要OkHttpClient,Hilt 会自动从容器中找。
核心原则:能用 @Inject 的尽量用 @Inject,只有第三方库才用 @Module + @Provides。这样代码更简洁,也更容易维护。
组件作用域:控制对象的生命周期
作用域决定了依赖的存活时间。Hilt 提供了几个内置的作用域:
| 作用域 | 对应组件 | 生命周期 |
|---|---|---|
@Singleton |
SingletonComponent | 整个应用生命周期 |
@ActivityScoped |
ActivityComponent | Activity 生命周期 |
@FragmentScoped |
FragmentComponent | Fragment 生命周期 |
@ViewModelScoped |
ViewModelComponent | ViewModel 生命周期 |
@ServiceScoped |
ServiceComponent | Service 生命周期 |
为什么要用作用域?
举个例子:你的网络请求库应该是全局单例,因为创建 OkHttpClient 很重。但某个 Activity 专用的数据缓存,就应该在 Activity 销毁时一起清理。
@ActivityScoped
class UserSession @Inject constructor(
private val context: Activity
) {
// 这个对象只在 Activity 存活期间存在
private val cache = mutableMapOf<String, Any>()
fun put(key: String, value: Any) {
cache[key] = value
}
fun get(key: String): Any? {
return cache[key]
}
}
注意:作用域不能乱用。如果你给一个依赖加了 @ActivityScoped,那么所有注入这个依赖的地方都会拿到同一个实例。如果某个 Fragment 想独立持有自己的实例,就不能用作用域,或者用更小的作用域。
Hilt 的组件层级
Hilt 的组件是有层级关系的。子组件可以访问父组件的依赖,但反过来不行。
这张图展示了 Hilt 的组件层级。从上到下依次是:SingletonComponent → ActivityComponent → FragmentComponent → ViewModelComponent。
每个子组件都可以访问父组件的依赖。比如 Fragment 可以拿到 Activity 的依赖,但 Activity 拿不到 Fragment 的。
实战:一个完整的 Hilt 示例
把上面学到的知识串起来,写一个完整的例子:
// 1. Application
@HiltAndroidApp
class MyApp : Application()
// 2. 网络模块
@Module
@InstallIn(SingletonComponent::class)
object NetworkModule {
@Provides
@Singleton
fun provideOkHttp(): OkHttpClient = OkHttpClient.Builder().build()
@Provides
@Singleton
fun provideRetrofit(client: OkHttpClient): Retrofit =
Retrofit.Builder()
.baseUrl("https://api.example.com")
.client(client)
.addConverterFactory(GsonConverterFactory.create())
.build()
@Provides
@Singleton
fun provideApi(retrofit: Retrofit): ApiService =
retrofit.create(ApiService::class.java)
}
// 3. Repository
class UserRepository @Inject constructor(
private val api: ApiService
) {
suspend fun getUser(): User = api.getUser()
}
// 4. ViewModel
@HiltViewModel
class MainViewModel @Inject constructor(
private val repo: UserRepository
) : ViewModel() {
private val _user = MutableStateFlow<User?>(null)
val user: StateFlow<User?> = _user.asStateFlow()
fun loadUser() {
viewModelScope.launch {
_user.value = repo.getUser()
}
}
}
// 5. Activity
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
private val viewModel: MainViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
viewModel.loadUser()
}
}
我的习惯:把 Module 按功能拆分,比如 NetworkModule、DatabaseModule、AnalyticsModule。每个 Module 只负责一类依赖,这样好找也好改。
避坑指南
我用 Hilt 这几年,踩过不少坑。分享几个最常见的:
- @HiltViewModel 必须配合 by viewModels() 使用:不能自己 new ViewModel,否则注入不生效。
- Fragment 的 @AndroidEntryPoint 别忘了:我经常写完 Activity 就忘了 Fragment 也要加。
- @Module 里的方法不要用 private:Hilt 是通过编译期生成代码来调用的,private 方法它调不了。
- 作用域要匹配:如果你在 SingletonComponent 里提供了一个 @ActivityScoped 的依赖,Hilt 会报错。因为父组件不能提供子组件作用域的依赖。
我曾经遇到的一个诡异问题:在 Fragment 里注入了一个 @ActivityScoped 的依赖,结果 Fragment 销毁重建后,拿到的还是旧的实例。后来才发现,@ActivityScoped 的实例在 Activity 销毁前一直存在。如果 Fragment 需要独立实例,要么不加作用域,要么用 @FragmentScoped。
总结
Hilt 的核心就三件事:
- @Inject:告诉 Hilt 怎么创建你的类
- @Module + @Provides:告诉 Hilt 怎么创建第三方库的类
- 作用域:控制依赖的生命周期
说白了,Hilt 就是一个帮你管理对象创建和生命周期的工具。用好了,代码清爽,测试方便。用不好,各种空指针和生命周期问题够你喝一壶的。
我个人建议:从小项目开始用 Hilt,慢慢体会它的设计思路。别一上来就搞复杂的多 Module 项目,容易把自己绕晕。
公众号:蓝海资料掘金营,微信deep3321