Dagger2的基本使用

240 阅读13分钟

依赖注入

我们知道在一个类中,通常会定义其他类型的变量,这个变量就是我们所说的“依赖“。

对一个类的变量进行初始化,有两种方式。第一种,这个类自己进行初始化;第二种,其他外部的类帮你进行初始化。

关键术语含义在 Dagger 2 中的体现
依赖(Dependency)类内部需要使用的外部对象任何要注入的字段 / 构造函数参数
控制反转(IoC)对象创建权从类自身“反转”到外部容器由 Dagger 生成的工厂 / Component 负责创建与注入
依赖注入(DI)通过 IoC 将依赖“注入”到目标对象字段注入、构造函数注入、方法注入
@Inject标记依赖接收点或可提供实例的构造函数@Inject lateinit var repo: Repository @Inject constructor(api: ApiService)
@Module / @Provides声明如何创建复杂对象@Module class NetModule {@Provides fun okHttp() = OkHttpClient()}
@Component桥梁:连接依赖提供者(Module)与注入目标DaggerAppComponent.create().inject(this)
作用域(如 @Singleton)限定实例生命周期同一 Component 内共享单例对象

1 | 为什么要依赖注入?

  1. 解耦

    • 类只关心“需要什么”,不再关心“如何创建”
  2. 可测试性

    • 依赖可在测试中轻松替换为 Mock
  3. 可维护性 / 复用

    • 复杂依赖关系集中声明,变更影响面小

2 | Dagger 2 工作流程

A(声明依赖<br>@Inject/@Module) --> B(编译期 APT)
B --> C(生成 *DaggerXxx* 工厂/图)
C --> D(运行时调用<br>Component.build())
D --> E(自动 new 对象并注入)
  • 编译期 APT 根据注解生成代码 → 0 反射、性能佳
  • 运行期 只负责调用生成好的工厂,完成注入

3 | 最小示例

// 依赖类
//Dagger 编译器扫描到这个 `@Inject` 构造函数,就知道要“造一个 `NetworkClient`”时,就调用这个构造函数。
// 它会先满足参数列表里所有依赖(`ApiService`、`Logger` 之类),再去 new `NetworkObject(...)`。
class NetworkClient @Inject constructor(ApiService apiService, Logger logge)     // 构造函数注入

// 注入目标
class MainActivity : AppCompatActivity() {
    @Inject lateinit var client: NetworkClient   // 字段注入

    override fun onCreate(saved: Bundle?) {
        super.onCreate(saved)
        DaggerAppComponent.create().inject(this) // 完成注入
    }
}

// Module(可选:当构造函数太复杂时提供)
@Module
class AppModule {
    @Provides fun baseUrl(): String = "https://api.xxx.com"
}

// Component
@Singleton
@Component(modules = [AppModule::class])
interface AppComponent {
    fun inject(activity: MainActivity)
}

4 | 作用域与单例

作用域注解效果何时使用
@SingletonComponent 级单例全局共享,如 OkHttp、数据库
自定义作用域(如 @ActivityScope自定义生命周期与 Subcomponent / Component dependencies 配合

注意:作用域生效的前提是:注解要同时出现在
1)@Component、2)@Module or @Provides、3)注入目标三处之一;且依赖必须由同一 Component 管理。


5 | 常见问题速答

问题简答
为什么 @Provides 方法/Module/Component 都要加同一作用域?让 Dagger 在编译期生成的工厂知晓“该对象应共享”,否则默认每次 new。
能用多态 (BaseActivity) 作为 inject 参数吗?不行。Dagger 以“具体类型”生成代码,需写 void inject(MainActivity a)
局部 vs 全局单例Component 在 Activity 内创建 → 局部 Component 在 Application 创建并持有 → 全局
Dagger 2 与 HiltHilt = Google 官方对 Dagger 的再封装,自动生成 Module/Component,简化模板代码。

一句话总结

Dagger 2 = 用编译期代码生成 (APT) 的“依赖工厂”,通过 @Inject + @Module/@Provides + @Component 三件套,把“创建依赖”这件事彻底交给框架,既性能友好,又易维护。

整个dagger2其实就是解决了为对象通过APT技术进行赋值的操作。

Dagger2的基本使用

首先先确定我们需要的元素有哪些:

image.png

Dagger 2 注入流程一图 & 三要素

  • Module:告诉 Dagger「我会如何创建 A、B、C 这些依赖」

  • Component:告诉 Dagger「把 A、B、C 串成一棵树/图,并在 inject(target) 时分发给目标」

  • Injection Target:告诉 Dagger「我需要这些依赖,请帮我注入」

┌────────────┐        ┌─────────────┐        ┌────────────┐
│  Module(s) │ ──▶   │ Component   │ ──▶   │  Target(s) │
│ @Provides  │        │  @Component │        │  @Inject   │
└────────────┘        └─────────────┘        └────────────┘
        ▲                                          │
        │  创建依赖对象                             │字段 / 构造函数注入
        └────────── 依赖实例被注入 ────────────────┘
角色作用样例
Object最终要注入的依赖DataObjectNetworkObject
@Module告诉 Dagger 如何 创建依赖@Module class NetworkModule { @Provides fun net() = NetworkObject() }
@Component桥梁 —— 连接 Module 与注入目标@Component(mods=[NetworkModule::class,DataModule::class]) interface MyComponent { fun inject(activity: MainActivity) }

核心步骤

  1. 声明依赖类

    class DataObject {}
    class NetworkObject {}
    
  2. 提供依赖

    @Module
    class DataModule {
        @Provides DataObject provideData() { return new DataObject(); }
    }
    
    @Module
    class NetworkModule {
        @Provides NetworkObject provideNet() { return new NetworkObject(); }
    }
    
  3. 建立组件

    @Component(modules = {DataModule.class, NetworkModule.class})
    interface MyComponent {
        void inject(MainActivity a);
        void inject(MainActivity2 a);
    }
    
  4. 在目标类中标记注入点

    • DaggerMyComponent 其实就是 Dagger 在编译期帮我们“生成”出来的 MyComponent 接口的实现类。它是由 Dagger 的注解处理器(dagger-compiler)在编译阶段自动生成的,名字约定为 Dagger + 你的 Component 接口名。
    public class MainActivity extends AppCompatActivity {
        @Inject NetworkObject net1;
        @Inject NetworkObject net2;
    
        protected void onCreate(Bundle s) {
            super.onCreate(s);
            DaggerMyComponent.create().inject(this);
        }
    }
    

为什么 inject 方法参数不能用父类或接口

原因说明
编译期代码生成Dagger 在 APT 阶段为 确切类型 生成注入代码,需要固定、完整的类信息。
类型擦除若用父类 / 接口,真实子类信息被擦除,编译器无法定位具体依赖图。
依赖解析一致性不同子类所需依赖可能不同,用基类会使 Dagger 无法确定注入内容,失去编译期安全。

结论void inject(MainActivity activity) 必须写 具体类;若多个子类需要注入,就为每个子类写一个 injectXXX(),或使用 Subcomponent/Hilt 等更高层方案来复用。

在具体的类中进行注入:

public class MainActivity extends AppCompatActivity {
    // 下边就是普通的方式,直接通过new方式生成一个对象。
//    NetworkObject networkObject = new NetworkObject();
//    NetworkObject networkObject2 = new NetworkObject();

    // 这下边就是通过Dagger2的注入方式来实现对象的生成。
    @Inject
    NetworkObject networkObject;
    @Inject
    NetworkObject networkObject2;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        // 方式一
        // DaggerMyComponent.create().injectMainActivity(this);

        // 方式二 使用注解处理器生成代码的细节,来完成的
        DaggerMyComponent.builder()
                .networkModule(new NetworkModule())
                .dataModule(new DataModule())
                .build()
                // 到这里,初始化了module和component
                .injectMainActivity(this);

        // 不一样的hashCode  答:非单例
        Log.i("zxc", networkObject.hashCode() + " MainActivity"); // 不一样的hashCode
        Log.i("zxc", networkObject2.hashCode() + " MainActivity");
    }

    public void click(View view) {
        startActivity(new Intent(this, MainActivity2.class));
    }
}

Dagger2生成对象的过程中,有两种方式,均在注释中。两种方式的区别在于,第二种可以配置更多的生成细节。

如何在Dagger中生成单例?

在 Dagger 2 中生成「单例」的两种做法

Dagger 单例两种场景

目标关键思路典型做法(见下方代码块)
局部单例同一 Component + 同一作用域注解↓ 代码块 ①
全局单例Application 启动时只创建一次 Component,并对外暴露 getter↓ 代码块 ②

代码块 ① —— 局部单例 (同一 Component 内共享)

// NetworkModule.java
@Singleton
@Module
class NetworkModule {
    @Provides @Singleton
    NetworkObject provideNet() {
        return new NetworkObject();
    }
}

// MyComponent.java
@Singleton
@Component(modules = {NetworkModule.class})
interface MyComponent {
    void inject(MainActivity a);
}
  • 作用域 @Singleton 同时标注在 Module / @Provides / Component
  • 只要依赖通过 同一个 MyComponent 注入,得到的 NetworkObject 都是同一实例;换一个 Component 就会重新创建。

代码块 ② —— 全局单例 (整个 App 进程共享)

// 1️⃣ Application 持有唯一 Component
@HiltAndroidApp            // 若不用 Hilt,可手动 new DaggerAppComponent
public class MyApplication extends Application {

    private MyComponent appComponent;

    @Override public void onCreate() {
        super.onCreate();
        appComponent = DaggerMyComponent           // 依然是局部单例配置
                       .builder()
                       .networkModule(new NetworkModule())
                       .build();                   // ← 只创建一次
    }

    public MyComponent getAppComponent() {
        return appComponent;
    }
}

// 2️⃣ Activity / Fragment 中使用
public class MainActivity extends AppCompatActivity {

    @Inject NetworkObject net1;
    @Inject NetworkObject net2;

    @Override protected void onCreate(Bundle b) {
        super.onCreate(b);
        ((MyApplication) getApplication())
                .getAppComponent()                // 全局唯一 Component
                .inject(this);                    // 完成注入

        Log.i("TAG", "hashCodes → " + net1.hashCode() + ", " + net2.hashCode());
    }
}
  • NetworkObject 依旧用 @Singleton 声明;
  • 因为 整个应用只构造一次 DaggerMyComponent@Singleton 对象自然变成“全局单例”。
  • 任意 Activity / Fragment 只要通过同一个 appComponent 注入,拿到的都是同一 NetworkObject

⚠️ 小结

  • 局部单例:限定在某个 Component 图内复用。
  • 全局单例:把那个“局部单例 Component” 提升到 Application,并只实例化一次。

生成局部单例

只需要在上述的代码中做以下修改即可:

NetworkModule:

@Singleton // 添加这个单例注解
@Module
public class NetworkModule {

    @Singleton  // 添加这个单例注解
    @Provides
    public NetworkObject providerNetworkObject(){
        return new NetworkObject();
    }
}

MyComponent:

@Singleton // 添加这个单例注解
@Component(modules = {NetworkModule.class, DatabaseModule.class})
public interface MyComponent {
    // 注意:这里的参数是不能用多态
    void injectMainActivity(MainActivity activity);

    void injectMainActivity2(MainActivity2 activity2);
}

总共有三个地方需要添加单例的注解,NetworkModule有两处,MyComponent有一处,但需要注意的是,这个只是局部单例,也就是在另外的Activity中,通过这种方式注入的对象,依然是一个新对象。

生成全局单例

生成全局单例需要在局部单例的基础上,再增加以下逻辑:

在自定义的 Application 中:

public class MyApplication extends Application {

    private MyComponent myComponent;

    @Override
    public void onCreate() {
        super.onCreate();
        // 就是把之前Activity的代码,拿上来了而已
        myComponent= DaggerMyComponent.builder()
                .httpModule(new HttpModule())
                .databaseModule(new DatabaseModule())
                .build();
    }

    public MyComponent getAppComponent(){
        return myComponent;
    }
}

然后再使用时,用下边的方式注入:

// 用MyApplication  == 全局单例
    ((MyApplication)getApplication())
            .getAppComponent()
            .injectMainActivity(this);

使用全局单例的情况下,变成了以下的结构:

image.png

现在的Activity的整体代码:

public class MainActivity extends AppCompatActivity {

    @Inject
    NetWorkObject netWorkObject;

    @Inject
    NetWorkObject netWorkObject2;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        // 方式一
        // DaggerMyComponent.create().injectMainActivity(this);

        // 方式二
        /*DaggerMyComponent.builder()
                .httpModule(new HttpModule())
                .databaseModule(new DatabaseModule())
                .build()
                // 到这里,初始化了module和component
                .injectMainActivity(this);*/

        // 方式三  用MyApplication  == 全局单例
        ((MyApplication)getApplication())
                .getAppComponent()
                .injectMainActivity(this);

        Log.i("zxc", netWorkObject.hashCode() + " MainActivity");
        Log.i("zxc", netWorkObject2.hashCode() + " MainActivity");
    }

    public void click(View view) {
        startActivity(new Intent(this, MainActivity2.class));
    }
}

构造函数注入

“构造函数注入”并不是“返回一个构造函数”,而是一种告诉 Dagger “我希望你通过这个构造函数来创建我这个类的实例” 的方式。


一、什么是构造函数注入?

在你要注入的类上,直接在它的构造函数前加上 @Inject,例如:

public class NetworkObject {
    @Inject
    public NetworkObject(ApiService apiService, Logger logger) {
        // Dagger 会自动给你传入 apiService、logger
    }
}

背后的含义

  • Dagger 编译器扫描到这个 @Inject 构造函数,就知道要“造一个 NetworkObject”时,就调用这个构造函数。
  • 它会先满足参数列表里所有依赖(ApiServiceLogger 之类),再去 new NetworkObject(...)

二、与 Module + @Provides 的区别

  • Module + @Provides

    @Module
    class NetworkModule {
      @Provides
      NetworkObject provideNetworkObject(ApiService api, Logger log) {
        return new NetworkObject(api, log);
      }
    }
    

    你手写工厂方法,告诉 Dagger “如果要 NetworkObject,就调用我这个方法”。

  • 构造函数注入

    public class NetworkObject {
        @Inject
        public NetworkObject(ApiService api, Logger logger) { … }
    }
    

    你不自己写工厂方法,Dagger 在编译期自动为你生成相当于上面 provideNetworkObject() 的逻辑。


三、使用场景

  1. 参数都是其他依赖(也都能被 Dagger 提供)时,推荐用构造函数注入:

    • 代码更少,模块更干净。
  2. 有特殊构造逻辑、或者你需要用到第三方库的类型,无法改它的构造函数时,才用 @Module + @Provides


四、底层原理

  • Dagger 的注解处理器在编译期间会为每个带 @Inject 构造函数的类生成一个 provider:

    // 伪代码,编译器生成
    Provider<NetworkObject> networkObjectProvider = () -> 
        new NetworkObject(apiServiceProvider.get(), loggerProvider.get());
    
  • 在 Component 的实现里,它就直接调用这一段代码来实例化并注入。


五、示例:省掉 Module

假设 ApiServiceLogger 也各自有 @Inject 构造函数,或者通过 Module 提供,那么你只需:

public class ApiService {
    @Inject public ApiService(/* ... */) { }
}

public class Logger {
    @Inject public Logger() { }
}

public class NetworkObject {
    @Inject public NetworkObject(ApiService api, Logger log) { … }
}

@Component
interface AppComponent {
    void inject(MainActivity activity);
}

然后在 MainActivity

@Inject NetworkObject networkObject;

@Override
protected void onCreate(...) {
    DaggerAppComponent.create().inject(this);
    // networkObject 已经通过构造函数注入并初始化完毕
}

这样一来,你完全不用写额外的 Module,就能把所有依赖串起来,Dagger 会自动调用那些 @Inject 构造函数去“造”对象。

学后测验

一、单选题(每题 4 分)

  1. 依赖注入(DI)的核心目的 是为了
    A. 提高应用启动速度
    B. 将对象创建职责从类内部转移到外部,降低耦合
    C. 实现多线程并发
    D. 简化 UI 绘制
    解析:DI/控制反转本质是把“依赖的初始化”交给外部容器,实现解耦。
  2. 在 Dagger 2 中,@Inject 常用于
    A. 标记 Module 类
    B. 标记需要注入的字段或构造函数
    C. 标记 Component 接口
    D. 声明作用域
    解析@Inject 既可放字段也可放构造函数,告诉 Dagger 该依赖需要被注入/创建。
  3. 下列关于 @Module@Provides 的说法,错误的是
    A. @Provides 方法必须返回 void
    B. @Provides 方法可以带参数,由 Dagger 提供这些参数
    C. @Module 类负责告诉 Dagger 如何构造对象
    D. 一个 Component 可引用多个 Module
    解析@Provides 必须返回实例,返回 void 无意义。
  4. 若想让 NetworkObject 在同一个 Component 生命周期内保持单例,应在 哪些地方 同时加 @Singleton(或自定义作用域)?
    A. Component 接口、对应 Module 类、以及提供该对象的 @Provides 方法
    B. 只有 Component
    C. 只有 Module
    D. 只在字段上
    解析:三处缺一不可,才能保证作用域一致。
  5. 以下哪种注入写法 不能通过编译(假设 MyActivity 继承 BaseActivity)?
    A. void injectMainActivity(MyActivity a);
    B. void injectMainActivity(BaseActivity a);
    C. void injectMainActivity2(MyActivity2 a);
    D. void injectMainActivity3(MyActivity3 a);
    解析:Dagger 生成代码按“确切类型”生成,不支持在 Component 中用多态参数。
  6. 对于 @Component.Builder 链式构建方式,以下哪句描述正确?
    A. 可在 builder 中显式传入自定义 Module 实例
    B. 只能使用 create(),不能自定义 builder
    C. builder 只能绑定一个 Module
    D. builder 不能设置作用域
    解析:Builder 提供 xxxModule(new XxxModule()) 等方法,灵活传参。

二、判断题(每题 3 分)

  1. (√)Dagger 2 的依赖解析与代码生成发生在编译期,而非运行期。
    解析:APT 会在 compile 时生成 DaggerXxx 类。
  2. (×)若两处 @Inject 字段属于同一 Component,Dagger 默认会返回同一实例(单例)。
    解析:若未声明作用域,Dagger 默认 每次都会 new,不是单例。
  3. (√)在 Android 中,将 Component 放到自定义 Application 并暴露 getter,可实现“全局单例”。
    解析:同一 Application 只实例化一次 Component,其作用域对象在整个应用进程内保持唯一。
  4. (×)@Provides 方法必须是 public
    解析:可为 internal/package-private,只要 Component 能访问即可。

三、多选题(每题 5 分)

  1. 在 Dagger 2 中,下列属于 典型依赖注入入口(Injection Target) 的有
    A. Activity/Fragment 字段
    B. @Module 中的 @Provides 方法
    C. 自定义类中的 @Inject 字段
    D. 构造函数上加 @Inject 的类实例
    解析:只要类上有 @Inject 字段或 @Inject 构造函数,都可被注入;@Provides 只是供应方。
  2. 若一个对象既被标注 @Singleton,又被多个 不同 Component 引用,而这些 Component 不是同一个依赖图,则
    A. 每个 Component 仍会各自持有一份独立单例
    B. 可以通过 @Component(dependencies = …) 方式让它们共享同一实例
    C. 若不做依赖关系声明,跨 Component 不会自动共享单例
    D. Application 内永远只有一个该类型实例
    解析@Singleton 受 Component 作用域限制;不同组件若互不依赖,各自生成;通过 dependencies/subcomponent 才能共享。

四、简答题(共 20 分,答案要点即可)

13 (10 分)

何谓“控制反转(IoC)”?请结合 Dagger 2 描述它如何体现 IoC。

参考要点

  • IoC:将对象创建/依赖解析的控制权,从类自身反转到外部框架(容器)。
  • 在 Dagger 2 中,对象不再 new 依赖,而是 声明依赖(@Inject) -> 框架在编译期生成工厂 -> 运行时由 Component 注入。开发者只关注“声明”,不关心“构造”;控制权被交给 Dagger,体现 IoC。

14 (10 分)

说明 Dagger 2 如何实现“局部单例”与“全局单例”。请给出关键步骤。

参考要点

  1. 局部单例

    • @Provides 方法、Module 类、Component 同时加同一作用域注解(如 @Singleton)。
    • 只要对象通过同一个 Component 注入,在该 Component 生命周期内都是同一实例。
  2. 全局单例

    • 将 Component 实例化放在 Application,并保存为成员。
    • 所有 Activity/Fragment 通过 ((App).component).inject() 获取依赖。
    • 因为整个应用只创建一次 Component,同作用域对象全局唯一。