🎯 为什么需要 Hilt?从 Dagger 的痛点说起
在 Hilt 出现之前,Android 开发者如果要使用依赖注入,最强大的选择是 Dagger。但 Dagger 的学习曲线非常陡峭,配置也相当复杂。你需要手动创建和管理大量的 Component、Module,还要理解复杂的作用域和依赖关系图。很多开发者在使用 Dagger 的过程中,常常会陷入自己搭建的"依赖迷宫"中,难以调试和维护。
Google 推出 Hilt 的目的,就是在不改变 Dagger 强大能力的前提下,针对 Android 开发场景进行"固化"和"简化"。它通过预定义好 Android 应用中的常用组件(如 Application、Activity、Fragment 等)及其生命周期,让开发者无需手动创建和管理这些组件,只需通过注解即可完成依赖注入。
🧱 Hilt 的核心概念
在开始使用之前,你需要理解 Hilt 的三大核心构件:
-
组件 (Components) :Hilt 为 Android 中的每个标准类(Application、Activity、Fragment、Service、View 等)都预定义了一个对应的 Dagger 组件。这些组件负责管理依赖的生命周期,并自动跟随 Android 组件的生命周期创建和销毁。例如:
SingletonComponent:伴随 Application 的生命周期。ActivityComponent:伴随 Activity 的生命周期。FragmentComponent:伴随 Fragment 的生命周期。ViewModelComponent:伴随 ViewModel 的生命周期。
-
作用域 (Scopes) :通过注解(如
@Singleton、@ActivityScoped)来限定依赖在对应组件内的唯一性。例如,用@Singleton标记的依赖在全局只有一个实例,而用@ActivityScoped标记的依赖在同一个 Activity 内是同一个实例。 -
模块 (Modules) :当你想注入的类不是你自己写的(如 Retrofit、OkHttpClient 等第三方库),或者是一个接口时,就需要用到 Hilt 模块。它是一个用
@Module注解的类,你需要在这个类中定义如何提供这些依赖的实例。同时,必须用@InstallIn注解指明这个模块安装在哪个组件中,从而让 Hilt 知道它的作用范围。
🚀 Hilt 的使用方法
1. 添加依赖与配置
首先,在项目根目录的 build.gradle 文件中添加 Hilt 的 Gradle 插件:
gradle
buildscript {
ext {
hilt_version = '2.48' // 请使用最新版本
}
dependencies {
classpath "com.google.dagger:hilt-android-gradle-plugin:$hilt_version"
}
}
然后在你的 app 模块的 build.gradle 文件中应用插件并添加依赖:
gradle
plugins {
id 'kotlin-kapt'
id 'dagger.hilt.android.plugin'
}
android {
// ...
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = '1.8'
}
}
dependencies {
implementation "com.google.dagger:hilt-android:$hilt_version"
kapt "com.google.dagger:hilt-compiler:$hilt_version"
}
kotlin-kapt 插件用于处理 Hilt 的编译时注解,这是代码生成的基础。
2. 初始化 Hilt:@HiltAndroidApp
所有使用 Hilt 的应用都必须有一个带有 @HiltAndroidApp 注解的 Application 类。这会触发 Hilt 的代码生成,生成一个全局的 Dagger 组件(即 SingletonComponent)的基类。
kotlin
@HiltAndroidApp
class MyApplication : Application() {
override fun onCreate() {
super.onCreate()
// 应用初始化
}
}
别忘了在 AndroidManifest.xml 中注册这个自定义的 Application 类。
3. 在 Android 类中注入:@AndroidEntryPoint
要在 Activity、Fragment、View、Service、BroadcastReceiver 中使用依赖注入,你需要为它们添加 @AndroidEntryPoint 注解。
kotlin
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
// 通过 @Inject 注解的字段,声明需要注入的依赖
@Inject
lateinit var userRepository: UserRepository
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// 此时 userRepository 已经被自动注入了,可以直接使用
println(userRepository.getUser())
}
}
需要注意的是,如果给 Fragment 添加了 @AndroidEntryPoint,那么依赖它的所有 Activity 也必须添加 @AndroidEntryPoint。
4. 提供依赖的三种方式
Hilt 提供了三种主要的方式来告诉它"如何创建某个类的实例"。
A. 构造函数注入:@Inject
对于你自己编写的、可以修改构造函数的类,这是最简单的方式。直接在类的构造函数上添加 @Inject 注解。
kotlin
// 告诉 Hilt:当需要 UserRepository 时,就通过这个构造函数创建它
class UserRepository @Inject constructor() {
fun getUser(): String = "User Data"
}
B. 模块 + @Provides
对于你无法修改构造函数的类(如 Retrofit、OkHttpClient 等第三方库),或者需要复杂构建逻辑的情况,你需要创建一个 Hilt 模块,并在其中定义一个用 @Provides 注解的方法。方法的返回值就是要提供的类型,Hilt 会调用这个方法获取实例。
kotlin
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class) // 告知此模块安装在全局单例组件中
object NetworkModule {
@Provides
@Singleton // 标记为全局单例
fun provideRetrofit(): Retrofit {
return Retrofit.Builder()
.baseUrl("https://api.example.com/")
.addConverterFactory(GsonConverterFactory.create())
.build()
}
}
C. 模块 + @Binds
当你想为一个接口 提供具体的实现类时,使用 @Binds。@Binds 注解的方法是一个抽象方法,参数是接口的实现类,返回值是接口本身。它告诉 Hilt,当需要接口时,就使用这个实现类。
kotlin
// 定义接口
interface UserService {
fun getUser(): String
}
// 实现类,用 @Inject 标记其构造函数,以便 Hilt 能创建它
class UserServiceImpl @Inject constructor() : UserService {
override fun getUser(): String = "User from service"
}
// 在模块中绑定接口与实现
@Module
@InstallIn(SingletonComponent::class)
abstract class ServiceModule {
@Binds
abstract fun bindUserService(impl: UserServiceImpl): UserService
}
5. 限定符:@Qualifier
当同一个类型需要提供多个不同的实例时,就需要用到限定符。例如,你可能需要两个不同的 Retrofit 实例(一个用于 GitHub API,一个用于其他 API)。Hilt 内置了 @ApplicationContext 和 @ActivityContext 两个限定符来区分不同的 Context。你也可以自定义限定符。
kotlin
// 定义两个限定符注解
@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class GithubRetrofit
@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class OtherRetrofit
@Module
@InstallIn(SingletonComponent::class)
object NetworkModule {
@GithubRetrofit
@Provides
@Singleton
fun provideGithubRetrofit(): Retrofit {
return Retrofit.Builder().baseUrl("https://api.github.com/").build()
}
@OtherRetrofit
@Provides
@Singleton
fun provideOtherRetrofit(): Retrofit {
return Retrofit.Builder().baseUrl("https://other.api.com/").build()
}
}
// 使用时,用对应的限定符注解字段
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
@Inject
@GithubRetrofit
lateinit var githubRetrofit: Retrofit
@Inject
@OtherRetrofit
lateinit var otherRetrofit: Retrofit
}
6. 为 ViewModel 注入:@HiltViewModel
Hilt 与 Jetpack ViewModel 的集成非常出色。你只需在 ViewModel 上添加 @HiltViewModel 注解,并在其构造函数中使用 @Inject,Hilt 就会自动帮你创建 ViewModel,并注入所需的依赖。你的 Activity/Fragment 无需再关心如何创建 ViewModel。
kotlin
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
import javax.inject.Inject
@HiltViewModel
class MainViewModel @Inject constructor(
private val userRepository: UserRepository
) : ViewModel() {
fun loadUser() {
// 使用 userRepository 获取数据
}
}
在 Activity 或 Fragment 中,你就可以像往常一样通过 by viewModels() 或 hiltNavGraphViewModels 获取 ViewModel 实例了,无需任何额外的工厂类。
🧪 Hilt 的测试支持
Hilt 的强大之处也体现在测试上。它提供了专门的测试 API,让你能轻松地在单元测试和插桩测试中替换依赖,实现隔离测试。
- 配置测试 :为测试类添加
@HiltAndroidTest注解和一个HiltAndroidRule,并使用HiltTestApplication作为测试的 Application。 - 注入依赖 :在测试类中,你可以像在生产代码中一样使用
@Inject来注入所需的对象。 - 替换模块 :Hilt 提供了
@TestInstallIn和@UninstallModules两种方式来替换生产代码中的模块。@TestInstallIn:用于定义一个全局的测试模块,替换掉整个生产模块。适合大多数测试场景。@UninstallModules:用于在单个测试类中,临时卸载某些生产模块,然后通过@BindValue等方式提供模拟实现。这种方式非常灵活,但可能会导致构建时间变慢,应谨慎使用。
kotlin
@HiltAndroidTest
@RunWith(AndroidJUnit4::class)
class MainActivityTest {
@get:Rule
val hiltRule = HiltAndroidRule(this)
@Inject
lateinit var userRepository: UserRepository // 注入真实或模拟的对象
@Before
fun setup() {
hiltRule.inject() // 执行注入
}
// ... 测试代码
}
⚙️ 原理简析:Hilt 是如何工作的?
Hilt 的核心原理是编译时注解处理与代码生成。
- 注解处理 :在编译时,Hilt 的注解处理器会扫描所有带有
@HiltAndroidApp、@AndroidEntryPoint、@Module、@Inject等注解的代码。 - 生成 Dagger 组件 :基于扫描结果,Hilt 会自动生成一系列标准的 Dagger 组件(如
SingletonComponent、ActivityComponent等),这些组件已经预先配置好了与 Android 组件生命周期的绑定关系。例如,ActivityComponent会在 Activity 的onCreate()时创建,在onDestroy()时销毁。 - 生成代码 :Hilt 还会为被
@AndroidEntryPoint标记的类生成一个基类。这个基类负责在合适的生命周期回调中(如onCreate())执行实际的注入操作,即从对应的 Dagger 组件中获取依赖,并赋值给带有@Inject注解的字段。 - 无运行时反射:所有这些工作都在编译时完成,生成的代码是纯粹的、高效的 Dagger 代码,因此在运行时几乎没有性能损耗。
💡 总结与最佳实践
- Hilt = Dagger 的 Android 最佳实践:它不是为了取代 Dagger,而是为了让开发者能以更低的成本享受到 Dagger 的强大能力。
- 几乎零模板代码 :你不再需要编写繁琐的
Component和工厂类,只需关注如何用注解声明依赖关系。 - 与 Jetpack 深度集成:对 ViewModel、Navigation、Compose 等都有很好的支持。
- 测试友好:提供了强大的测试 API,让依赖替换变得异常简单。
- 性能优化技巧 :合理使用作用域注解(如
@Singleton)来管理对象生命周期;在模块设计上保持"单一职责",便于测试替换;对于大量测试,优先使用@TestInstallIn来提升构建速度。
Hilt 的出现,极大地改善了 Android 开发的依赖注入体验。希望这篇讲解能帮助你更好地理解和使用 Hilt,写出更健壮、更可维护的代码。