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 核心逻辑。你看一遍,基本就明白整个流程了。
实战建议
说了这么多,给你几个我实际项目中的建议:
- 别一开始就上 Dagger。小项目用
ServiceLocator模式就够了。项目超过 20 个类,再考虑引入。 - Module 要精简。一个 Module 只负责一个领域。我见过有人把网络、数据库、SharedPreferences 全塞一个 Module 里,那叫一个乱。
- Component 不要太多。通常 AppComponent + ActivityComponent 就够了。FragmentComponent 除非必要,否则别加。
- 编译报错别慌。Dagger 的错误信息虽然长,但仔细看最后一行,通常能定位到问题。
小技巧:如果你不确定某个依赖能不能被 Dagger 找到,在 Android Studio 里按住 Ctrl 点击 @Inject 或 @Provides,Dagger 会跳转到生成的工厂类。能跳转,说明没问题。
Dagger2 的学习曲线确实陡。但一旦上手,你会发现它带来的好处远大于学习成本。代码更干净,依赖更清晰,测试也更方便。
好了,这一章就到这里。记住一句话:依赖注入不是银弹,但它能帮你写出更健壮的代码。