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 依赖注入层次结构 @HiltAndroidApp (Application) @AndroidEntryPoint (Activity) @AndroidEntryPoint (Fragment) @Inject ViewModel / Repository / UseCase 全局单例 Activity 作用域 Fragment 作用域 无作用域

核心要点: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 的依赖注入流程可以归纳为三步:

  1. @Inject 标记构造函数,告诉 Hilt 如何创建对象。
  2. @Module + @Provides 提供无法直接标记构造函数的依赖。
  3. @AndroidEntryPoint 开启注入,用 @Inject 字段接收依赖。

我个人觉得,Hilt 最大的价值不是「少写代码」,而是「强制你思考依赖关系」。当你把每个类的依赖都显式地通过构造函数声明出来,代码的耦合度自然就降下来了。这比任何设计模式都管用。

好了,这一章的内容就到这里。记住:依赖注入不是银弹,但它能帮你把代码组织得更清晰。下一章我们会聊聊 Hilt 的高级用法,比如自定义作用域和组件。


公众号:蓝海资料掘金营,微信deep3321