14、Hilt依赖注入:Hilt原理、@HiltAndroidApp、@AndroidEntryPoint、@Inject、@Module、@Provides、实战:依赖注入
依赖注入,说白了就是「我要什么,你给我什么」,而不是我自己去 new。在 Android 开发里,手动管理对象创建和生命周期,项目一大了就乱成一锅粥。Hilt 就是 Google 官方给 Android 量身定做的依赖注入框架,基于 Dagger,但门槛低了很多。
我记得刚接触 Dagger 那会儿,光是理解 Component、Subcomponent、Module 就花了一周。后来 Hilt 出来,我第一时间就切过去了。为什么?因为它把那些繁琐的模板代码都封装好了,你只需要关注业务本身。
Hilt 的核心原理
Hilt 本质上是一个编译期注解处理器。它会在编译时扫描你的注解,生成对应的 Dagger 组件代码。你写的 @HiltAndroidApp、@AndroidEntryPoint 这些注解,最终都会被翻译成 Dagger 的 @Component 和 @Subcomponent。
它的工作流程可以概括为三步:
- 注解标记:告诉 Hilt 哪些类需要注入,哪些类可以被提供。
- 编译期生成:Hilt 处理器生成 Dagger 的组件代码。
- 运行时注入:在 Activity、Fragment 等生命周期回调中自动完成注入。
下面这张图能帮你快速理解 Hilt 的层次结构:
核心要点:Hilt 的组件层次与 Android 生命周期天然对齐。Application 级别的单例,Activity 级别的作用域,Fragment 级别的子作用域,层层递进,自动管理。
@HiltAndroidApp
这是 Hilt 的入口。你需要在 Application 类上加上这个注解。它会在编译时生成一个父组件,所有其他组件都挂在这个下面。
@HiltAndroidApp
class MyApplication : Application() {
override fun onCreate() {
super.onCreate()
// 这里不需要手动初始化 Hilt,注解已经帮你做了
}
}
嗯,这里要注意:Application 类必须加上这个注解,否则 Hilt 不会工作。我曾经见过有人忘了加,结果运行时报错说找不到组件,排查了半天。
@AndroidEntryPoint
这个注解用在 Activity、Fragment、Service、BroadcastReceiver 上。告诉 Hilt:「这个类需要注入依赖,帮我生成对应的子组件。」
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
@Inject
lateinit var userRepository: UserRepository
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// 注意:注入发生在 super.onCreate() 之后
// 所以这里可以直接使用 userRepository
}
}
避坑指南:我曾经在 Fragment 里用 @AndroidEntryPoint 时,忘了在 Fragment 的宿主 Activity 上也加上 @AndroidEntryPoint。结果 Fragment 的注入一直失败。记住:Fragment 的宿主 Activity 也必须标注,因为 Fragment 的组件依赖于 Activity 的组件。
@Inject
@Inject 有两个用途:
- 标记构造函数:告诉 Hilt 这个类可以通过构造函数创建。
- 标记字段:告诉 Hilt 把这个依赖注入进来。
// 标记构造函数
class UserRepository @Inject constructor(
private val apiService: ApiService
) {
fun getUser(): User { ... }
}
// 标记字段
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
@Inject
lateinit var userRepository: UserRepository
}
说白了,@Inject 就是 Hilt 的「我要」和「我能给」的统一标记。你想想看,一个注解解决了提供方和消费方的问题,是不是很简洁?
@Module 和 @Provides
有些类你不能直接改构造函数,比如第三方库的类、接口的实现、或者需要复杂构建逻辑的对象。这时候就需要 @Module 和 @Provides 出场了。
@Module
@InstallIn(SingletonComponent::class)
object NetworkModule {
@Provides
@Singleton
fun provideOkHttpClient(): OkHttpClient {
return OkHttpClient.Builder()
.connectTimeout(30, TimeUnit.SECONDS)
.build()
}
@Provides
@Singleton
fun provideRetrofit(client: OkHttpClient): Retrofit {
return Retrofit.Builder()
.baseUrl("https://api.example.com/")
.client(client)
.addConverterFactory(GsonConverterFactory.create())
.build()
}
}
这里有几个关键点:
@Module标记这是一个提供依赖的模块。@InstallIn指定这个模块安装在哪个组件中。比如SingletonComponent表示全局单例。@Provides标记方法,告诉 Hilt 这个方法可以创建某个类型的实例。@Singleton表示这个实例在整个应用中只创建一次。
个人习惯:我一般会把网络相关的依赖放在一个 NetworkModule 里,数据库相关的放在 DatabaseModule 里。这样模块划分清晰,后期维护起来也方便。
实战:依赖注入
光说不练假把式。我们来看一个完整的实战例子。假设我们要做一个用户信息展示页面,需要从网络获取数据。
第一步:定义依赖
// 网络接口
interface ApiService {
@GET("user/{id}")
suspend fun getUser(@Path("id") id: String): User
}
// 仓库层
class UserRepository @Inject constructor(
private val apiService: ApiService
) {
suspend fun getUser(id: String): User {
return apiService.getUser(id)
}
}
// ViewModel
class UserViewModel @Inject constructor(
private val userRepository: UserRepository
) : ViewModel() {
private val _user = MutableStateFlow<User?>(null)
val user: StateFlow<User?> = _user.asStateFlow()
fun loadUser(id: String) {
viewModelScope.launch {
_user.value = userRepository.getUser(id)
}
}
}
第二步:提供网络依赖
@Module
@InstallIn(SingletonComponent::class)
object NetworkModule {
@Provides
@Singleton
fun provideApiService(retrofit: Retrofit): ApiService {
return retrofit.create(ApiService::class.java)
}
@Provides
@Singleton
fun provideRetrofit(): Retrofit {
return Retrofit.Builder()
.baseUrl("https://api.example.com/")
.addConverterFactory(GsonConverterFactory.create())
.build()
}
}
第三步:在 Activity 中使用
@AndroidEntryPoint
class UserActivity : AppCompatActivity() {
@Inject
lateinit var viewModel: UserViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_user)
// 直接使用 viewModel
viewModel.loadUser("123")
viewModel.user.observe(this) { user ->
// 更新 UI
}
}
}
你看,整个过程下来,我们完全没有手动去 new 任何东西。所有的依赖都由 Hilt 在编译期自动生成代码来提供。这就是依赖注入的魅力——解耦、可测试、易维护。
实战总结:Hilt 的依赖注入流程可以归纳为三步:
- 用
@Inject标记构造函数,告诉 Hilt 如何创建对象。 - 用
@Module+@Provides提供无法直接标记构造函数的依赖。 - 用
@AndroidEntryPoint开启注入,用@Inject字段接收依赖。
我个人觉得,Hilt 最大的价值不是「少写代码」,而是「强制你思考依赖关系」。当你把每个类的依赖都显式地通过构造函数声明出来,代码的耦合度自然就降下来了。这比任何设计模式都管用。
好了,这一章的内容就到这里。记住:依赖注入不是银弹,但它能帮你把代码组织得更清晰。下一章我们会聊聊 Hilt 的高级用法,比如自定义作用域和组件。
公众号:蓝海资料掘金营,微信deep3321