一、什么是 Dagger 2?为什么要用它?
Dagger 2 是一个完全在编译阶段生成依赖注入代码的框架,全程不使用反射,因此非常适合对运行时性能敏感的 Android 开发 。它的名字 "Dagger" 其实是 "DAG-er" 的谐音,代表着其核心数据结构——有向无环图 (Directed Acyclic Graph) 。
在没有依赖注入框架时,我们经常需要手动 new 出依赖对象,比如 new A(new B(new C())),这使得代码耦合度高、难以测试和维护 。Dagger 2 旨在解决这些问题,它的主要优势包括:
- 解耦:将对象的创建和使用分离,类不再关心依赖如何被创建,只需声明“我需要什么” 。
- 可测试性:在单元测试中,可以轻松地使用 Mock 对象替换真实依赖 。
- 生命周期管理:通过自定义
@Scope注解,可以精确控制对象在特定范围内的复用(如整个应用生命周期或某个 Activity 的生命周期)。 - 编译时校验:如果依赖关系图存在问题(例如,某个依赖找不到),会在编译时直接报错,而不是等到运行时才崩溃 。
二、Dagger 2 的核心概念与实战
Dagger 2 的使用主要围绕几个核心注解展开。为了更好地理解,我们可以把它想象成一个网购过程 :
- 商品:你需要使用的依赖对象。
- 包裹(
@Module):负责打包商品,告诉快递员如何准备商品。 - 快递员(
@Component):负责将包裹中的商品送到你手上,是连接提供方和使用方的桥梁。 - 收货人(
@Inject):你,在需要商品的地方签收。
下面我们通过一个具体的 UserRepository 示例来走一遍流程。
1. 添加依赖
首先,在模块的 build.gradle 文件中添加 Dagger 2 的依赖和注解处理器 。
dependencies {
implementation 'com.google.dagger:dagger:2.x'
kapt 'com.google.dagger:dagger-compiler:2.x' // Kotlin 项目使用 kapt,Java 项目使用 annotationProcessor
}
2. 定义依赖:@Inject 构造方法
这是最简单、最优先推荐的方式。在目标类的构造方法上添加 @Inject 注解,告诉 Dagger 2 如何创建该类的实例 。
// UserLocalDataSource.kt
class UserLocalDataSource @Inject constructor() {
fun getUserData(): String = "Local User Data"
}
// UserRemoteDataSource.kt
class UserRemoteDataSource @Inject constructor() {
fun getUserData(): String = "Remote User Data"
}
现在,我们有一个 UserRepository,它依赖于上述两个数据源。
// UserRepository.kt
class UserRepository @Inject constructor(
private val localDataSource: UserLocalDataSource,
private val remoteDataSource: UserRemoteDataSource
) {
fun fetchData(): String {
return "${localDataSource.getUserData()} and ${remoteDataSource.getUserData()}"
}
}
3. 定义连接桥梁:@Component
@Component 注解一个接口,Dagger 2 会在编译时自动生成这个接口的实现类(通常以 Dagger 开头,如 DaggerAppComponent)。它负责将依赖注入到目标对象中 。
// AppComponent.kt
@Component
interface AppComponent {
// 定义注入方法,参数指定将依赖注入到哪个类的实例中
fun inject(activity: MainActivity)
}
4. 执行注入:在目标类中使用
在需要依赖的地方(如 MainActivity),使用 @Inject 注解标记字段,并在合适的时机(如 onCreate)调用 DaggerAppComponent 进行注入 。
// MainActivity.kt
class MainActivity : AppCompatActivity() {
@Inject
lateinit var userRepository: UserRepository
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
// DaggerAppComponent 是编译后自动生成的类
DaggerAppComponent.create().inject(this)
// 现在 userRepository 已经被成功注入,可以直接使用
println(userRepository.fetchData())
}
}
完成以上步骤后,运行项目,你会发现 userRepository 已经被成功实例化,并且它依赖的两个 DataSource 也被自动注入。这一切都得益于我们在构造方法上添加的 @Inject。
5. 处理第三方依赖和复杂情况:@Module 与 @Provides
当我们需要注入的类是我们无法修改源码的第三方库(如 Retrofit、OkHttpClient),或者构造方法需要动态参数时,@Inject 就无能为力了。这时就需要 @Module 和 @Provides 出场 。
假设我们需要提供一个 Retrofit 实例。
首先,创建一个 @Module 注解的类,并在其中定义 @Provides 注解的方法,方法的返回值就是我们要提供的依赖类型 。
// AppModule.kt
@Module
class AppModule {
@Provides
fun provideBaseUrl(): String = "https://api.example.com/"
@Provides
fun provideRetrofit(baseUrl: String): Retrofit {
return Retrofit.Builder()
.baseUrl(baseUrl)
.build()
}
}
然后,在 @Component 中声明使用这个 Module。
// AppComponent.kt
@Component(modules = [AppModule::class]) // 声明使用 AppModule
interface AppComponent {
fun inject(activity: MainActivity)
}
Dagger 2 会从 AppModule 中查找所有 @Provides 方法,并利用它们来满足依赖需求。
6. 处理同类型多实例:@Named 或自定义 @Qualifier
如果一个类型有多个不同的实现(比如开发环境和生产环境的 ApiService),我们需要告诉 Dagger 2 如何区分它们。这时可以使用 @Named 注解 。
// AppModule.kt
@Module
class AppModule {
@Provides
@Named("debug") // 使用 @Named 进行标记
fun provideDebugApiService(): ApiService = DebugApiService()
@Provides
@Named("release")
fun provideReleaseApiService(): ApiService = ReleaseApiService()
}
在需要注入的地方,同样使用 @Named 指定要注入哪一个。
// SomeClass.kt
class SomeClass @Inject constructor(
@Named("debug") private val apiService: ApiService // 明确指定需要 debug 版本的 ApiService
) {
// ...
}
@Named 是 Dagger 2 内置的一种 @Qualifier。你也可以创建自定义的 @Qualifier 注解,使代码更具语义 。
7. 管理对象生命周期:@Scope
默认情况下,Dagger 2 每次注入都会创建一个新的实例。如果我们希望某个对象在整个应用生命周期内是单例,或者在同一个 Activity 内复用,就需要使用 @Scope 注解 。
Dagger 2 内置了一个 @Singleton 注解。要让它生效,需要同时在 @Component 和 @Provides 方法(或 @Inject 构造方法)上使用它。
// 在 Component 上标记 @Singleton
@Singleton
@Component(modules = [AppModule::class])
interface AppComponent {
fun inject(activity: MainActivity)
}
// AppModule.kt
@Module
class AppModule {
@Provides
@Singleton // 同样标记 @Singleton,表明提供的是单例
fun provideRetrofit(): Retrofit {
return Retrofit.Builder().baseUrl("...").build()
}
}
关键理解:@Singleton 的作用范围与 @Component 的生命周期绑定。在上面的例子中,只要我们在 Application 中只创建一次 AppComponent,那么 provideRetrofit 方法也只会调用一次,从而保证了 Retrofit 实例在整个应用中唯一 。你也可以自定义 @ActivityScoped、@FragmentScoped 等 @Scope 注解,实现更细粒度的生命周期控制。
三、Dagger 2 的工作原理
Dagger 2 最神奇的地方在于,它没有在运行时使用任何反射,所有的依赖查找和实例化代码都是在编译时生成的 。这得益于 Java 的注解处理器 (Annotation Processor)。
整个流程可以概括为:注解 -> 注解处理器 -> 生成源代码 -> 编译运行。
- 编译时扫描:当我们执行编译(
./gradlew build)时,Dagger 2 的注解处理器(dagger-compiler)会扫描项目中所有的@Inject、@Module、@Component等注解 。 - 构建依赖图:注解处理器会分析所有被注解的类和方法,构建出一个庞大的有向无环图 (DAG) 。这个图清晰地表达了每个对象的依赖关系:
A依赖于B,B依赖于C和D... 如果图中存在循环依赖(如 A 依赖 B,B 又依赖 A),或者某个依赖无法被满足,注解处理器就会在编译时抛出错误。 - 生成源代码:在构建好依赖图后,Dagger 2 会为每个
@Component接口生成对应的实现类(如DaggerAppComponent),并为每个被@Inject注解构造方法的类生成一个工厂类 (Factory)(如UserRepository_Factory),为每个被@Inject注解字段的类生成一个成员注入器类 (MembersInjector)(如MainActivity_MembersInjector)。- 工厂类 (Factory):它的职责非常单一,就是负责创建某个类型的实例,并通过
get()方法返回。UserRepository_Factory内部会调用UserRepository的构造方法,而构造方法所需的参数(如UserLocalDataSource和UserRemoteDataSource)则由它们自己的工厂提供。 - 成员注入器类 (MembersInjector):它的职责是负责将依赖注入到目标对象的字段中。
MainActivity_MembersInjector的injectMembers()方法内部,会调用UserRepository_Factory的get()方法来获取实例,并将其赋值给MainActivity中被@Inject注解的userRepository字段。
- 工厂类 (Factory):它的职责非常单一,就是负责创建某个类型的实例,并通过
- 运行时调用:当我们在代码中调用
DaggerAppComponent.create().inject(this)时,实际上就是在执行 Dagger 2 为我们生成的这些高效、无反射的普通 Java 代码。DaggerAppComponent内部会组织并调用一系列的Factory和MembersInjector,最终完成依赖的创建和注入 。
简单来说,Dagger 2 将原本需要你手动编写的、繁琐的“new 对象”和“set 对象”的代码,通过注解处理器自动帮你生成了。它就像一个代码生成器,在幕后为你打理好一切,让你能够专注于业务逻辑。
总结与展望
Dagger 2 是一个强大但学习曲线较陡峭的依赖注入框架。它的核心在于编译时代码生成,通过 @Inject、@Module、@Component、@Scope、@Qualifier 等注解,构建一个清晰、可复用的依赖关系图,极大地提升了 Android 应用的可维护性和可测试性 。
虽然现在 Google 官方推荐使用基于 Dagger 2 封装的 Hilt,它大大简化了 Dagger 2 在 Android 上的使用难度,提供了更简单的 API 和更好的 Android 组件集成 。但深入理解 Dagger 2 的原理和核心概念,依然是掌握 Hilt 的坚实基础。当你了解了背后的 DAG 和编译时生成代码的思想,再看 Hilt 的 @HiltAndroidApp 和 @AndroidEntryPoint,会有一种豁然开朗的感觉。