Dagger/Hilt 依赖注入:简化 Android DI
Dagger/Hilt 依赖注入:简化 Android DI
依赖注入(DI)是现代 Android 开发的基础技术之一,它能帮助我们构建松耦合、易测试、可维护的应用。本教程将从零开始,带你掌握 Google 推荐的 DI 方案 —— Hilt,并理解其背后的 Dagger 原理。无需预备知识,我们将用最直观的例子带你上手。
1. 为什么需要依赖注入?
在开发中,一个类经常需要依赖其他类的实例才能工作。比如 Car 需要 Engine:
class Car {
private val engine = Engine()
fun start() {
engine.ignite()
}
}
这种直接 new 的方式存在几个问题:
- 强耦合:
Car与Engine的具体实现绑定,无法灵活替换(如换成ElectricEngine)。 - 测试困难:无法模拟
Engine进行单元测试。 - 生命周期管理复杂:多次创建相同实例,内存浪费或状态不一致。
依赖注入 的核心思想是:将对象的创建和依赖关系交给外部容器管理,类只需声明自己需要什么,而不负责创建。上述代码可变为:
class Car @Inject constructor(private val engine: Engine) {
fun start() { engine.ignite() }
}
这样,Engine 的实例由容器注入,Car 类变得纯粹、可测试。
2. 什么是 Hilt?
Hilt 是基于 Dagger 的依赖注入库,专为 Android 设计。Dagger 的强大在于编译时生成代码,无运行时性能损耗,但学习曲线陡峭。Hilt 对 Dagger 进行了高层封装,简化了 Component、Module 的配置,让开发者用少量注解即可实现全应用范围的 DI。
Hilt 的特点:
- Android 专属优化:内置了对 Application、Activity、Fragment、ViewModel 等 Android 组件的支持。
- 自动注入生命周期感知:比如
ViewModel的注入自动与SavedStateHandle关联。 - 标准化与简化:通过约定优于配置,大幅减少样板代码。
- 无缝集成其他 Jetpack 库:如 Navigation Compose。
3. 环境搭建
3.1 项目级 build.gradle
在项目根目录的 build.gradle 中添加 Hilt 的 Gradle 插件:
buildscript {
ext.hilt_version = "2.50"
dependencies {
classpath "com.google.dagger:hilt-android-gradle-plugin:$hilt_version"
}
}
3.2 模块级 build.gradle
在 app 模块的 build.gradle 中应用插件并添加依赖:
plugins {
id 'com.android.application'
id 'kotlin-kapt'
id 'dagger.hilt.android.plugin'
}
android { ... }
dependencies {
implementation "com.google.dagger:hilt-android:$hilt_version"
kapt "com.google.dagger:hilt-compiler:$hilt_version"
// 可选:用于测试
testImplementation 'com.google.dagger:hilt-android-testing:$hilt_version'
kaptTest 'com.google.dagger:hilt-compiler:$hilt_version'
}
注意:从 Hilt 2.40 起也可使用 KSP(Kotlin Symbol Processing)替代 kapt,以获得更快编译速度。本教程以 kapt 演示。
4. 快速入门:从 Application 到 ViewModel
4.1 创建 Application 并标记 @HiltAndroidApp
所有使用 Hilt 的应用都需要一个自定义 Application 类,并添加 @HiltAndroidApp 注解。这会将 Application 变为 Hilt 的依赖容器根。
@HiltAndroidApp
class MyApp : Application()
别忘了在 AndroidManifest.xml 中声明:
<application android:name=".MyApp" ...>
4.2 让你的 Activity 能被注入
要让 Activity 获得注入能力,需标记 @AndroidEntryPoint:
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
// 后续可注入依赖
}
同理,Fragment 也可标注 @AndroidEntryPoint,但其宿主 Activity 也必须标注。
4.3 注入一个简单依赖
现在我们注入一个 Repository 类。假设项目中有一个 UserRepository:
class UserRepository @Inject constructor() {
fun getUser(): String = "John Doe"
}
关键点:@Inject constructor() 告诉 Hilt 如何创建 UserRepository 实例。
在 MainActivity 中声明一个字段并注入:
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
@Inject lateinit var userRepository: UserRepository
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
Log.d("Hilt", "User: ${userRepository.getUser()}")
}
}
@Inject 注解的字段由 Hilt 在 onCreate() 之前自动初始化,因此可直接使用。
5. 深入依赖提供方式
并非所有类都能直接在构造函数上加 @Inject(例如第三方库、接口等)。这时需要借助 Hilt Module。
5.1 使用 @Module 和 @Provides
假设我们有一个接口 AnalyticsService:
interface AnalyticsService {
fun logEvent(event: String)
}
class FirebaseAnalytics @Inject constructor() : AnalyticsService {
override fun logEvent(event: String) { /*...*/ }
}
我们想注入 AnalyticsService,需要告诉 Hilt 当遇到该接口时使用哪个实现。
创建一个用 @Module 和 @InstallIn 注释的类:
@Module
@InstallIn(SingletonComponent::class) // 说明模块的生命周期范围
abstract class AnalyticsModule {
@Binds
abstract fun bindAnalyticsService(
firebaseAnalytics: FirebaseAnalytics
): AnalyticsService
}
@InstallIn(SingletonComponent::class):将模块提供的依赖绑定到 SingletonComponent(应用级单例),这意味着依赖在应用生命周期内只创建一次。@Binds:用于接口-实现的绑定。因为方法的参数FirebaseAnalytics上已有@Inject constructor,Hilt 知道如何创建它。
如果你需要提供无法用 @Inject 创建的第三方类,使用 @Provides:
@Module
@InstallIn(SingletonComponent::class)
object NetworkModule {
@Provides
fun provideOkHttpClient(): OkHttpClient {
return OkHttpClient.Builder()
.connectTimeout(30, TimeUnit.SECONDS)
.build()
}
}
@Provides 方法体包含实例化逻辑,适合需要复杂构建或第三方库的情况。
5.2 常用组件生命周期
@InstallIn 可指定多种 Android 组件作用域:
| 组件 | 描述 |
|---|---|
SingletonComponent |
应用全局单例 |
ActivityRetainedComponent |
跨配置变更存活(如 ViewModel) |
ViewModelComponent |
绑定到 ViewModel 的生命周期 |
ActivityComponent |
绑定到 Activity 生命周期 |
FragmentComponent |
绑定到 Fragment 生命周期 |
选择合适的 scope 可以避免内存泄漏并优化性能。
6. 注入 ViewModel
Hilt 对 Android Architecture Components 提供了原生支持。要注入 ViewModel,只需给 ViewModel 添加 @HiltViewModel 注解,并在 Activity 或 Fragment 中正常声明。
6.1 创建可注入的 ViewModel
@HiltViewModel
class MainViewModel @Inject constructor(
private val userRepository: UserRepository
) : ViewModel() {
fun getUserName(): String = userRepository.getUser()
}
6.2 在 Activity 中使用
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
private val viewModel: MainViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// 使用 viewModel
}
}
by viewModels() 会自动感知 @HiltViewModel,并从 Hilt 容器中获取依赖构造 ViewModel。
7. 高级特性速览
7.1 依赖作用域自定义
你可以通过自定义作用域注解(如 @ActivityScoped)控制依赖的生命周期与 Activity 一致。Hilt 提供了几个预定义作用域:@Singleton、@ActivityScoped、@ViewModelScoped 等。
@Module
@InstallIn(ActivityComponent::class) // 模块安装到 ActivityComponent
object AdapterModule {
@ActivityScoped
@Provides
fun provideSomeAdapter(): SomeAdapter = SomeAdapter()
}
使用 @ActivityScoped 后,同一 Activity 内多次注入会得到同一个实例。
7.2 注入 Context 与资源
Hilt 预绑定了 Application 和 Activity 上下文,可以直接注入:
@AndroidEntryPoint
class MyFragment : Fragment() {
@ApplicationContext lateinit var appContext: Context
@ActivityContext lateinit var activityContext: Context
}
7.3 多重绑定(Set 与 Map)
有时你需要注入一组相同类型的依赖。Hilt 支持将多个 @Provides/@Binds 收集到 Set 或 Map 中:
@Module
@InstallIn(SingletonComponent::class)
object PluginModule {
@IntoSet
@Provides
fun providePluginOne(): Plugin = PluginOne()
@IntoSet
@Provides
fun providePluginTwo(): Plugin = PluginTwo()
}
// 注入时
class CentralManager @Inject constructor(
val plugins: Set<@JvmSuppressWildcards Plugin>
)
7.4 与 Navigation Compose 的集成
Hilt 完全支持 Jetpack Compose,通过 hiltViewModel() 可在 Composable 中获取 ViewModel:
@Composable
fun MyScreen(viewModel: MainViewModel = hiltViewModel()) {
// ...
}
需要添加依赖 androidx.hilt:hilt-navigation-compose。
8. 测试中的依赖替换
Hilt 提供便捷的测试支持,可替换真实依赖为测试替身。
8.1 配置测试 Application
创建一个测试专用的 Application 类并标注 @HiltTestApplication。
8.2 在测试中使用 @UninstallModules 和 @BindValue
@HiltAndroidTest
@UninstallModules(AnalyticsModule::class) // 移除生产模块
class ExampleTest {
@get:Rule
var hiltRule = HiltAndroidRule(this)
@BindValue @JvmField
val fakeAnalytics: AnalyticsService = FakeAnalyticsService()
@Before
fun init() {
hiltRule.inject()
}
@Test
fun testSomething() { ... }
}
这样测试环境就会使用我们创建的假对象,无需修改生产代码。
9. 总结与最佳实践
- 一切从
@HiltAndroidApp开始,它是 Hilt 的入口。 - 能用
@Inject constructor的类优先用,无需写 Module,更简洁。 - 接口绑定用
@Binds,第三方库/需要创建逻辑的用@Provides,并选用合适的组件@InstallIn。 - ViewModel 加
@HiltViewModel,享受自动注入SavedStateHandle的好处。 - 关注作用域,避免不必要的单例导致的内存泄漏或测试问题。
- 利用 Hilt Gradle 插件,它会在编译时检查依赖图的正确性,错误早期暴露。
- 迁移 Dagger 项目时,可逐步将
@Component或@Subcomponent转换为 Hilt 的组件体系,官方提供迁移指南。
Hilt 让 Android 依赖注入变得简单、标准化且安全。掌握它,不仅提高开发效率,也让你的应用架构更加健壮。赶快在你的下一个项目中尝试吧!
延伸阅读