Android Dagger2笔记

8 阅读8分钟

一、什么是 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

当我们需要注入的类是我们无法修改源码的第三方库(如 RetrofitOkHttpClient),或者构造方法需要动态参数时,@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)

整个流程可以概括为:注解 -> 注解处理器 -> 生成源代码 -> 编译运行

  1. 编译时扫描:当我们执行编译(./gradlew build)时,Dagger 2 的注解处理器(dagger-compiler)会扫描项目中所有的 @Inject@Module@Component 等注解 。
  2. 构建依赖图:注解处理器会分析所有被注解的类和方法,构建出一个庞大的有向无环图 (DAG) 。这个图清晰地表达了每个对象的依赖关系:A 依赖于 BB 依赖于 CD... 如果图中存在循环依赖(如 A 依赖 B,B 又依赖 A),或者某个依赖无法被满足,注解处理器就会在编译时抛出错误。
  3. 生成源代码:在构建好依赖图后,Dagger 2 会为每个 @Component 接口生成对应的实现类(如 DaggerAppComponent),并为每个被 @Inject 注解构造方法的类生成一个工厂类 (Factory)(如 UserRepository_Factory),为每个被 @Inject 注解字段的类生成一个成员注入器类 (MembersInjector)(如 MainActivity_MembersInjector)。
    • 工厂类 (Factory):它的职责非常单一,就是负责创建某个类型的实例,并通过 get() 方法返回。UserRepository_Factory 内部会调用 UserRepository 的构造方法,而构造方法所需的参数(如 UserLocalDataSourceUserRemoteDataSource)则由它们自己的工厂提供。
    • 成员注入器类 (MembersInjector):它的职责是负责将依赖注入到目标对象的字段中。MainActivity_MembersInjectorinjectMembers() 方法内部,会调用 UserRepository_Factoryget() 方法来获取实例,并将其赋值给 MainActivity 中被 @Inject 注解的 userRepository 字段。
  4. 运行时调用:当我们在代码中调用 DaggerAppComponent.create().inject(this) 时,实际上就是在执行 Dagger 2 为我们生成的这些高效、无反射的普通 Java 代码。DaggerAppComponent 内部会组织并调用一系列的 FactoryMembersInjector,最终完成依赖的创建和注入 。

简单来说,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,会有一种豁然开朗的感觉。