79、依赖注入之Dagger2:@Module、@Component、@Inject、作用域、组件依赖

依赖注入,说白了就是「我要什么,你给我什么」。

不用自己 new 对象,不用到处找实例。这在大型项目里特别重要。我接手过一个老项目,里面到处都是 new XXX(),改一个构造方法,十几个地方报错。那感觉,真叫一个酸爽。

Dagger2 就是来解决这个问题的。它用注解帮你生成代码,编译期就把依赖关系理清楚。运行时几乎零开销,性能没得说。

核心注解:@Inject

@Inject 是 Dagger2 的敲门砖。它有两个用法:

  • 标记构造方法:告诉 Dagger,这个类我能被创建
  • 标记成员变量:告诉 Dagger,我需要这个依赖
public class UserRepository {
    private final ApiService apiService;

    @Inject
    public UserRepository(ApiService apiService) {
        this.apiService = apiService;
    }
}

public class LoginViewModel {
    @Inject
    UserRepository userRepository;

    public LoginViewModel() {
        // 不用手动赋值
    }
}

嗯,这里要注意:@Inject 标记的构造方法,参数也会被 Dagger 自动注入。所以 ApiService 也得能被 Dagger 创建,否则编译报错。

核心原则:@Inject 只能标记你自己能修改的类。第三方库的类(比如 Retrofit、OkHttp),你改不了构造方法,就得用 @Module。

@Module 和 @Provides

遇到第三方类怎么办?用 @Module

Module 就像一个「工厂车间」。你告诉 Dagger:「这个对象我不会造,但我写了个方法帮你造」。

@Module
public class NetworkModule {

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

    @Provides
    Retrofit provideRetrofit(OkHttpClient client) {
        return new Retrofit.Builder()
                .baseUrl("https://api.example.com")
                .client(client)
                .build();
    }
}

我在项目中遇到过一个问题:两个 @Provides 方法返回同一个类型,Dagger 直接编译报错。后来用 @Named 或者自定义 @Qualifier 才解决。这个后面会讲。

个人习惯:我会把 Module 按功能拆分。网络相关放 NetworkModule,数据库相关放 DatabaseModule。别把所有东西塞到一个 Module 里,那比面条代码还难维护。

@Component:连接器

Module 提供了原料,Inject 标记了需求。谁来搭桥?@Component

Component 是一个接口,Dagger 会帮你生成实现类。你只需要调用它,就能拿到完整的对象图。

@Component(modules = {NetworkModule.class})
public interface AppComponent {
    void inject(MainActivity activity);
    void inject(LoginActivity activity);
}

使用的时候:

public class MyApp extends Application {
    private AppComponent appComponent;

    @Override
    public void onCreate() {
        super.onCreate();
        appComponent = DaggerAppComponent.create();
    }

    public AppComponent getAppComponent() {
        return appComponent;
    }
}

你想想看,如果没有 Dagger,你得手动创建 OkHttpClient、Retrofit,然后传给每个 ViewModel。现在只需要一行 DaggerAppComponent.create(),整个世界清净了。

作用域:@Singleton 和自定义 Scope

默认情况下,每次注入都是新对象。但有些对象我们希望全局唯一,比如 OkHttpClient。

这时候就需要作用域了。

@Module
public class NetworkModule {

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

@Component(modules = {NetworkModule.class})
@Singleton
public interface AppComponent {
    // ...
}

注意:@Singleton 必须同时在 @Provides@Component 上标记。少一个都不行,编译直接报错。

除了 @Singleton,你还可以自定义作用域:

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

@ActivityScope
@Component(dependencies = {AppComponent.class})
public interface ActivityComponent {
    void inject(MainActivity activity);
}

我曾经踩过一个坑:两个 Activity 共用同一个 @ActivityScope 的 Component,结果数据串了。后来才明白,每个 Activity 应该有自己的 Component 实例。

避坑指南:作用域不是「全局单例」的代名词。它只是告诉 Dagger:「在这个 Component 的生命周期内,对象只创建一次」。Component 销毁了,对象也就没了。

组件依赖

一个 App 不可能只有一个 Component。通常我们会拆成:

  • AppComponent:全局单例,提供 OkHttp、Retrofit、数据库等
  • ActivityComponent:每个 Activity 一个,提供页面级别的依赖
  • FragmentComponent:更细粒度的作用域

组件之间怎么通信?用 dependencies

@Component(modules = {NetworkModule.class})
@Singleton
public interface AppComponent {
    // 暴露给子组件的依赖
    OkHttpClient okHttpClient();
    Retrofit retrofit();
}

@ActivityScope
@Component(dependencies = {AppComponent.class}, modules = {ActivityModule.class})
public interface ActivityComponent {
    void inject(MainActivity activity);
}

这里有个关键点:子组件只能使用父组件暴露的方法。如果 AppComponent 没有暴露 okHttpClient(),子组件就拿不到。

我个人习惯把暴露的方法统一放在一个接口里,比如 AppDependencies,这样更清晰。

知识体系总览

下面这张图,是我梳理的 Dagger2 核心逻辑。你看一遍,基本就明白整个流程了。

Dagger2 核心流程 @Inject 构造方法 标记「我能被创建」 @Module + @Provides 提供第三方依赖 @Inject 成员变量 标记「我需要依赖」 @Component 编译生成 DaggerXXXComponent Activity / Fragment / ViewModel 自动注入所有 @Inject 字段 @Singleton / @ActivityScope 组件依赖 dependencies

实战建议

说了这么多,给你几个我实际项目中的建议:

  1. 别一开始就上 Dagger。小项目用 ServiceLocator 模式就够了。项目超过 20 个类,再考虑引入。
  2. Module 要精简。一个 Module 只负责一个领域。我见过有人把网络、数据库、SharedPreferences 全塞一个 Module 里,那叫一个乱。
  3. Component 不要太多。通常 AppComponent + ActivityComponent 就够了。FragmentComponent 除非必要,否则别加。
  4. 编译报错别慌。Dagger 的错误信息虽然长,但仔细看最后一行,通常能定位到问题。

小技巧:如果你不确定某个依赖能不能被 Dagger 找到,在 Android Studio 里按住 Ctrl 点击 @Inject@Provides,Dagger 会跳转到生成的工厂类。能跳转,说明没问题。

Dagger2 的学习曲线确实陡。但一旦上手,你会发现它带来的好处远大于学习成本。代码更干净,依赖更清晰,测试也更方便。

好了,这一章就到这里。记住一句话:依赖注入不是银弹,但它能帮你写出更健壮的代码