A
第 28 章JAVA45 分钟

依赖注入:Dagger 2 完整实战

Dagger 2 完整实战:核心注解 @Inject / @Provides / @Module / @Component、@Singleton 与自定义 Scope、@Qualifier 限定符、Component 依赖与 Subcomponent、AndroidInjector 以及测试中替换 Module。

学习目标

  • 掌握 Dagger 2 核心注解的语义与用法
  • 理解 @Singleton 与自定义 Scope 的作用域控制
  • 能够使用 @Qualifier 区分同类型多个绑定
  • 使用 AndroidInjector 简化 Activity / Fragment 注入
  • 在测试中通过替换 Module 提供假依赖

本章定位:Dagger 2 是 Java + Views 项目中依赖注入(DI)的成熟方案,也是 Hilt 的底层基础。Kotlin 项目中 Google 推荐 Hilt(基于 Dagger 的封装),但纯 Java 项目无法使用 Hilt(Hilt 依赖 Kotlin 协程与 Kotlin 注解处理)。因此 Dagger 2 仍是 Java 存量项目 DI 的最佳实践。Kotlin 迁移时可在过渡期继续使用 Dagger 2,再逐步评估是否迁移到 Hilt。

与 Part 8-10 的对应关系:Kotlin + Compose 路线请使用 Hilt,参见 Part 8-10 Kotlin + Compose 相关章节。

学习目标

  • 掌握 Dagger 2 核心注解 @Inject / @Provides / @Module / @Component 的语义与用法;
  • 理解 @Singleton 与自定义 Scope 的作用域控制;
  • 能够使用 @Qualifier 区分同类型多个绑定;
  • 使用 AndroidInjector 简化 Activity / Fragment 注入;
  • 在测试中通过替换 Module 提供假依赖。

依赖注入:Dagger 2 完整实战

为什么需要依赖注入

依赖注入(Dependency Injection, DI)的核心思想是 “调用方不创建依赖,由容器提供依赖”。在没有 DI 的代码里,类往往 new 出自己的依赖:

public class LoginPresenter {
    private final LoginManager manager = new LoginManager(new HttpService());
}

这导致:

  • 难以测试:替换 LoginManager 或 HttpService 必须改源码;
  • 耦合度高:高层模块依赖低层模块的具体实现;
  • 复用困难:依赖创建逻辑散落各处。

Dagger 2 是 Android 生态中最成熟的 编译期 DI 框架:通过注解处理器在编译期生成连接代码,没有运行时反射开销,错误在编译期暴露。

依赖配置

plugins {
    id "com.android.application" version "8.5.0"
}

android {
    compileSdk 34
    // Java 8+ 必须开启
    compileOptions {
        sourceCompatibility JavaVersion.VERSION_1_8
        targetCompatibility JavaVersion.VERSION_1_8
    }
}

dependencies {
    implementation "androidx.appcompat:appcompat:1.7.0"

    // Dagger 2 核心
    implementation "com.google.dagger:dagger:2.51.1"
    annotationProcessor "com.google.dagger:dagger-compiler:2.51.1"
    // dagger.android(可选,用于 Activity/Fragment 注入)
    implementation "com.google.dagger:dagger-android:2.51.1"
    implementation "com.google.dagger:dagger-android-support:2.51.1"
    annotationProcessor "com.google.dagger:dagger-android-processor:2.51.1"
}

核心注解

@Inject:标注注入点

@Inject 用在三处:

  1. 构造方法:告诉 Dagger 用这个构造方法创建实例(最常用);
  2. 字段:让 Dagger 在创建实例后给字段赋值;
  3. 方法:让 Dagger 在创建实例后调用方法注入参数。
public class LoginManager {
    @Inject
    public LoginManager(HttpService http, UserCache cache) {
        // 构造方法注入,Dagger 会自动解析 HttpService 与 UserCache
    }
}

public class LoginActivity extends AppCompatActivity {
    @Inject LoginManager loginManager; // 字段注入
}

构造方法注入是首选,便于单元测试时直接 new 实例。

@Module + @Provides:无法构造注入时的提供者

接口、第三方类、Context 等无法在构造方法上加 @Inject,需用 @Module 类中的 @Provides 方法提供:

@Module
public class NetworkModule {
    @Provides
    public HttpService provideHttpService(OkHttpClient client) {
        return new HttpServiceImpl(client);
    }

    @Provides
    public OkHttpClient provideOkHttpClient() {
        return new OkHttpClient.Builder()
                .connectTimeout(10, TimeUnit.SECONDS)
                .build();
    }
}

@Provides 方法的参数同样由 Dagger 解析,形成依赖图。

@Component:组装依赖图

@Component 把多个 @Module 装配成一个可执行依赖图,并暴露入口方法:

@Component(modules = {NetworkModule.class, LoginModule.class})
public interface AppComponent {
    // 方式 1:直接返回顶级依赖
    LoginManager loginManager();

    // 方式 2:注入字段(用于无法构造注入的类,如 Activity)
    void inject(LoginActivity activity);
}

编译时 Dagger 生成 DaggerAppComponent,使用方式:

AppComponent component = DaggerAppComponent.builder()
        .networkModule(new NetworkModule())
        .build();
LoginManager manager = component.loginManager();

完整最小示例

public class HttpService { }                // 接口/类
public class UserCache { }                  // 简单 POJO

public class LoginManager {                 // 业务类
    @Inject
    public LoginManager(HttpService http, UserCache cache) {
        // ...
    }
}

@Module
public class LoginModule {
    @Provides
    public HttpService provideHttpService() { return new HttpService(); }

    @Provides
    public UserCache provideUserCache() { return new UserCache(); }
}

@Component(modules = LoginModule.class)
public interface AppComponent {
    LoginManager loginManager();
}

// 调用
AppComponent c = DaggerAppComponent.create();
LoginManager m = c.loginManager();

@Singleton 与自定义 Scope

@Singleton

@Singleton 是 Dagger 内置的作用域,标注 @Provides 方法或 @Inject 构造方法后,该实例在同一个 Component 内 只创建一次:

@Module
public class NetworkModule {
    @Provides
    @Singleton
    public OkHttpClient provideOkHttpClient() {
        return new OkHttpClient.Builder().build();
    }
}

@Component(modules = NetworkModule.class)
@Singleton  // Component 必须也声明对应 Scope,否则编译报错
public interface AppComponent {
    OkHttpClient client();
}

注意 @Singleton 仅在 当前 Component 实例范围 内单例。如果每次都 DaggerAppComponent.create(),会得到不同的“单例”。通常把 AppComponent 放在 Application 中复用。

自定义 Scope

@Singleton 本质只是一个自定义 Scope,可以创建自己的 Scope 来表达更细粒度的生命周期:

@Scope
@Retention(RetentionPolicy.RUNTIME)
public @interface ActivityScope { }

@Scope
@Retention(RetentionPolicy.RUNTIME)
public @interface FragmentScope { }
@ActivityScope
@Subcomponent(modules = LoginModule.class)
public interface LoginComponent {
    void inject(LoginActivity activity);
}

@Module(subcomponents = LoginComponent.class)
public abstract class LoginActivityBinding { }

约定俗成:Scope 名称与 Component 生命周期对应(ActivityScope 对应 Activity 子组件,FragmentScope 对应 Fragment 子组件)。

@Qualifier:区分同类型多个绑定

当同一类型有多个绑定(如多个 String 配置项、生产 vs 调试 OkHttpClient),用 @Qualifier 注解区分:

@Qualifier
@Retention(RetentionPolicy.RUNTIME)
public @interface ApiKey { }

@Qualifier
@Retention(RetentionPolicy.RUNTIME)
public @interface DebugHttp { }

@Module
public class NetworkModule {
    @Provides
    @ApiKey
    public String provideApiKey() { return "secret-key"; }

    @Provides
    @DebugHttp
    public OkHttpClient provideDebugClient() {
        return new OkHttpClient.Builder()
                .addInterceptor(new HttpLoggingInterceptor().setLevel(Level.BODY))
                .build();
    }
}

public class ApiService {
    private final String apiKey;
    @Inject
    public ApiService(@ApiKey String apiKey) { this.apiKey = apiKey; }
}

如果忘了限定符,Dagger 在编译期直接报错 “Multiple … bindings”,避免运行时混淆。

也可以用 Dagger 内置的 @Named("xxx"),但自定义注解有编译期类型检查与重命名友好性。

Component 依赖与 Subcomponent

Component Dependencies

通过 dependencies 让一个 Component 复用另一个 Component 已暴露的依赖:

@Component(modules = {AppModule.class})
@Singleton
public interface AppComponent {
    UserCache userCache(); // 必须显式暴露,否则下游拿不到
}

@Component(modules = {LoginModule.class}, dependencies = AppComponent.class)
@ActivityScope
public interface LoginComponent {
    void inject(LoginActivity activity);
}

下游 Component 调用方式:

AppComponent app = ...;
LoginComponent login = DaggerLoginComponent.builder()
        .appComponent(app)
        .build();

Subcomponent

Subcomponent 是更紧密的子图,能直接访问父 Component 所有依赖,无需显式暴露:

@ActivityScope
@Subcomponent(modules = LoginModule.class)
public interface LoginComponent {
    void inject(LoginActivity activity);

    @Subcomponent.Builder
    interface Builder {
        LoginComponent build();
    }
}

@Module(subcomponents = LoginComponent.class)
public abstract class LoginActivityModule {
    @Provides
    @IntoMap
    @ActivityKey(LoginActivity.class)
    abstract AndroidInjector.Factory<? extends Activity>
        bindLoginFactory(LoginComponent.Builder builder);
}

子组件适合父子生命周期强包含的场景(如 Activity 隶属于 App)。Component dependencies 适合两个独立图复用关系。

AndroidInjector:dagger.android 简化

直接为每个 Activity 写 inject() 与 Component 是繁琐的,dagger.android 通过 @ContributesAndroidInjector 自动生成对应 Subcomponent:

public class App extends Application implements HasAndroidInjector {
    @Inject DispatchingAndroidInjector<Object> androidInjector;

    @Override
    public AndroidInjector<Object> androidInjector() {
        return androidInjector;
    }

    @Override
    public void onCreate() {
        super.onCreate();
        DaggerAppComponent.create().inject(this);
    }
}

@Singleton
@Component(modules = {AppModule.class, AndroidInjectionModule.class})
public interface AppComponent {
    void inject(App app);
}

@Module
public abstract class ActivityBindingModule {
    @ActivityScope
    @ContributesAndroidInjector(modules = LoginModule.class)
    abstract LoginActivity contributeLoginActivity();
}
public class LoginActivity extends AppCompatActivity {
    @Inject LoginManager loginManager;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        AndroidInjection.inject(this); // 注入字段
        super.onCreate(savedInstanceState);
        // ...
    }
}

AndroidInjectionModule 来自 dagger.android,提供 DispatchingAndroidInjector 的支撑代码。

测试中替换 Module

依赖注入最大的好处之一是 可测试性。在测试时通过提供一个测试 Module 来替换真实依赖:

生产 Module

@Module
public class NetworkModule {
    @Provides
    @Singleton
    public ApiService provideApiService(OkHttpClient client) {
        return new RealApiService(client);
    }

    @Provides
    @Singleton
    public OkHttpClient provideOkHttpClient() {
        return new OkHttpClient.Builder().build();
    }
}

@Component(modules = NetworkModule.class)
@Singleton
public interface AppComponent {
    ApiService apiService();
    void inject(LoginPresenter presenter);
}

测试 Module

@Module
public class TestNetworkModule {
    @Provides
    @Singleton
    public ApiService provideApiService() {
        return new FakeApiService(); // 返回固定假数据
    }

    @Provides
    @Singleton
    public OkHttpClient provideOkHttpClient() {
        return new OkHttpClient(); // 不需要真实网络
    }
}

@Component(modules = TestNetworkModule.class)
@Singleton
public interface TestAppComponent extends AppComponent {
    // 注意:测试 Component 必须用相同签名复写所有用到的入口
    ApiService apiService();
    void inject(LoginPresenter presenter);
}

单元测试

public class LoginPresenterTest {
    private TestAppComponent component;

    @Before
    public void setUp() {
        component = DaggerTestAppComponent.create();
    }

    @Test
    public void loginSuccess_callsShowSuccess() {
        LoginPresenter p = new LoginPresenter();
        component.inject(p);
        p.login("admin", "123456");
        // 断言逻辑(FakeApiService 永远返回成功)
    }
}

关键约束:测试 Module 必须与生产 Module 提供完全相同的依赖键(同类型 + 同 Qualifier),否则编译期报错。这也是 Dagger 编译期检查的价值之一。

常见坑与最佳实践

  1. Component 忘记加 Scope:@Provides @Singleton 的依赖,对应 Component 必须也标 @Singleton,否则编译报错。
  2. Subcomponent 没在父 Module 声明:Subcomponent 必须通过父 Component 的某个 @Module(subcomponents = ...) 声明才能被 Dagger 识别。
  3. 在 Application 中每次 create 新 Component:会让 @Singleton 失效,应在 Application 字段里缓存一个实例。
  4. 直接暴露 Activity 子组件:Activity 注入应通过 AndroidInjection.inject(this),不要在 AppComponent 上手动 inject(LoginActivity)。
  5. 用 @Named 字符串易拼错:自定义 @Qualifier 注解比 @Named("debug") 更安全,编译期可发现拼写错误。
  6. 测试 Component 与生产 Component 签名不一致:测试 Component 必须复写所有需要的入口方法签名。
  7. 循环依赖:A 构造依赖 B,B 构造依赖 A,Dagger 在编译期直接报错,需要拆解为字段注入或引入 Provider。
  8. Module 方法写成 private:@Provides 方法必须可被生成代码访问,不能是 private,否则编译失败。

章节小结

本章完整覆盖了 Dagger 2 的核心注解、@Singleton 与自定义 Scope、@Qualifier 限定符、Component 依赖与 Subcomponent 两种组装方式、AndroidInjector 简化 Activity/Fragment 注入,以及测试中替换 Module 提供假依赖。Dagger 2 的最大价值在于编译期检查与零运行时开销,配合合理的 Module 拆分能让测试替换非常自然。

下一章预告

下一章将进入异步编程领域,使用 RxJava 3:基础类型(Observable / Flowable / Single / Maybe / Completable)、常用操作符(map / flatMap / filter / concatMap)、线程调度(subscribeOn / observeOn)、背压策略、与 Android Lifecycle 互操作(RxJavaAndroid)、内存泄漏与 dispose / CompositeDisposable。