Dagger2从入门到放弃
写在前面
尝试用ds写技术博文,写的过程中也算是温故知新了~
今日题目:Dagger2
文章目录
- 一、前置知识准备
- 理解依赖注入(DI)的核心概念
- 掌握 Java/Kotlin 关键语法
- 二、核心组件解析
- 1.四大核心注解
- 2.作用域(Scope)机制
- 3.依赖图(Dependency Graph)原理
- 核心原理总结
- 三、进阶实战
Dagger2 系统学习路径
一、前置知识准备
理解依赖注入(DI)的核心概念
依赖注入(Dependency Injection,DI)的核心概念是将对象之间的依赖关系从代码内部转移到外部容器管理,从而实现代码解耦、提升可维护性和可测试性。
1. 控制反转(Inversion of Control, IoC)
定义:程序的控制权从内部转移到外部容器或框架,由容器管理对象的生命周期和依赖关系。
与DI的关系:DI是IoC的一种实现方式。IoC强调“控制权反转”,而DI通过“注入依赖”具体实现这一点。
2. 依赖注入的三种方式
构造函数注入:通过构造函数传递依赖项。
优点:强制依赖在对象创建时明确,保证对象始终处于有效状态。
示例:
public class UserService {
private final UserRepository repository;
public UserService(UserRepository repository) {
this.repository = repository;
}
}
属性注入:通过公共属性或Setter方法设置依赖。
优点:灵活性高,适合可选依赖或动态变更。
示例:
public class UserService {
private UserRepository repository;
public void setRepository(UserRepository repository) {
this.repository = repository;
}
}
方法注入:通过方法参数传递依赖。
适用场景:依赖仅在特定方法中使用,或需要运行时动态确定。
示例:
public class UserService {
public void process(UserRepository repository) {
// 使用repository执行操作
}
}
3. 解耦与面向接口编程
解耦:通过依赖抽象(接口或抽象类)而非具体实现,减少组件间的直接耦合。
示例:
public interface UserRepository {
User findById(String id);
}
public class MySqlUserRepository implements UserRepository { /* ... */ }
public class UserService {
private final UserRepository repository;
public UserService(UserRepository repository) {
this.repository = repository; // 依赖抽象,而非具体类
}
}
4. 依赖注入容器(DI Container)
- 作用:自动化管理对象的创建、依赖解析和生命周期。
- 功能:
-
- 注册:绑定接口到具体实现(如IUserRepository → MySqlUserRepository)。
-
- 解析:自动创建对象并注入所需依赖。
-
- 生命周期管理:支持单例(Singleton)、瞬态(Transient)、作用域(Scoped)等模式。
掌握 Java/Kotlin 关键语法
注解处理器(APT)的工作原理
1. 编译阶段触发
- APT 在 Java 编译过程中被触发,通常在
javac
编译器的编译阶段执行。 - 编译器会扫描源代码中的注解,并将其传递给注册的注解处理器。
2. 注解扫描
- 编译器会扫描源代码中的所有注解,并生成一个抽象语法树(Abstract Syntax Tree, AST)。
- 这些注解信息会被封装成
Element
对象,供注解处理器使用。
3. 注解处理器注册
- 注解处理器是通过实现
javax.annotation.processing.Processor
接口来定义的。 - 注解处理器需要注册到编译器中,通常通过
META-INF/services/javax.annotation.processing.Processor
文件来实现。
4. 注解处理
- 注册的注解处理器会依次处理扫描到的注解。
- 注解处理器可以通过
ProcessingEnvironment
获取编译上下文信息,如文件管理器、类型检查器等。 - 注解处理器可以通过
RoundEnvironment
获取当前轮次处理的注解元素。
5. 生成代码或资源
- 注解处理器可以根据注解信息生成新的源代码文件、资源文件或修改现有的代码。
- 生成的代码会被编译器进一步编译,成为最终编译输出的一部分。
6. 多轮处理
- 注解处理可能会进行多轮(Round),每一轮处理结束后,编译器会检查是否有新的注解生成。
- 如果有新的注解生成,编译器会启动新一轮的处理,直到没有新的注解生成为止。
7. 编译完成
- 当所有注解处理器处理完毕,且没有新的注解生成时,编译器会完成编译过程,生成最终的
.class
文件。
Kotlin 中 @JvmStatic 在 Module 中的应用
在 Kotlin 的 Module 中,@JvmStatic
注解主要用于将 companion object
或 object
中的成员暴露为 Java 兼容的静态成员,方便 Java 代码调用。它在工具类、单例模式和常量定义等场景中非常有用,能够提升 Kotlin 与 Java 混合开发的兼容性。
@JvmStatic
的作用
- 兼容 Java 调用:Kotlin 中的
companion object
成员默认不是静态的,Java 调用时需要先访问Companion
实例。@JvmStatic
可以将其暴露为真正的静态成员,方便 Java 代码直接调用。 - 简化访问方式:通过
@JvmStatic
,Java 代码可以直接通过类名访问方法或属性,而不需要通过Companion
。
@JvmStatic
在 Module 中的应用场景
在 Module 中,@JvmStatic
通常用于以下场景:
- 工具类或工具方法:在 Module 中提供静态工具方法,方便其他模块(尤其是 Java 模块)调用。
- 单例模式:在 Module 中实现单例模式时,通过
@JvmStatic
提供静态访问方式。 - 常量定义:在 Module 中定义常量时,通过
@JvmStatic
暴露为静态常量。
示例代码
1. 工具类中的 @JvmStatic
// 在 Module 中定义一个工具类
class MathUtils {
companion object {
@JvmStatic
fun add(a: Int, b: Int): Int {
return a + b
}
}
}
在 Java 中调用:
// Java 代码调用
int result = MathUtils.add(2, 3); // 直接通过类名调用
2. 单例模式中的 @JvmStatic
// 在 Module 中实现单例模式
class AppConfig private constructor() {
companion object {
@JvmStatic
val instance: AppConfig by lazy { AppConfig() }
}
fun getConfigValue(): String {
return "Config Value"
}
}
在 Java 中调用:
// Java 代码调用
String value = AppConfig.instance.getConfigValue();
3. 常量定义中的 @JvmStatic
// 在 Module 中定义常量
class AppConstants {
companion object {
@JvmStatic
val MAX_COUNT: Int = 100
}
}
在 Java 中调用:
// Java 代码调用
int maxCount = AppConstants.MAX_COUNT;
注意事项
@JvmStatic
只能用于companion object
或object
中的成员:普通类中的成员不能使用@JvmStatic
。- Kotlin 中优先使用
object
或companion object
:Kotlin 本身推荐使用object
或companion object
来实现单例或静态成员,而不是直接使用静态成员。 @JvmStatic
不会改变 Kotlin 中的调用方式:在 Kotlin 中,companion object
的成员仍然需要通过类名或companion
访问,@JvmStatic
主要是为了兼容 Java。
二、核心组件解析
1.四大核心注解
1. @Module
- 作用:定义一个提供依赖的模块。
@Module
类中包含一些用@Provides
注解标记的方法,这些方法负责创建和提供依赖对象。 - 实现原理:
- Dagger 2 在编译时会扫描所有
@Module
注解的类。 - 这些类中的
@Provides
方法会被提取出来,生成对应的工厂类(Factory),用于创建依赖实例。 - 这些工厂类会被集成到
@Component
生成的代码中,供依赖注入时使用。
- Dagger 2 在编译时会扫描所有
- 使用场景:当某个依赖无法通过构造函数注入(例如第三方库的类)时,可以使用
@Module
来提供这些依赖。 - 示例:
@Module public class NetworkModule { @Provides OkHttpClient provideOkHttpClient() { return new OkHttpClient(); } }
- 编译后,Dagger 2 会生成一个
NetworkModule_ProvideOkHttpClientFactory
类,用于创建OkHttpClient
实例。
2. @Provides
- 作用:在
@Module
类中标记方法,表示该方法用于提供某个依赖实例。 - 实现原理:
- Dagger 2 在编译时会为每个
@Provides
方法生成一个工厂类(Factory)。 - 这些工厂类实现了
Provider<T>
接口,用于在运行时创建依赖实例。 - 当需要注入某个依赖时,Dagger 2 会调用对应的工厂类来创建实例。
- Dagger 2 在编译时会为每个
- 使用场景:当需要手动创建依赖对象时,使用
@Provides
注解的方法。 - 示例:
@Module public class NetworkModule { @Provides Retrofit provideRetrofit(OkHttpClient client) { return new Retrofit.Builder() .baseUrl("https://api.example.com") .client(client) .build(); } }
- 编译后,Dagger 2 会生成一个
NetworkModule_ProvideRetrofitFactory
类,用于创建Retrofit
实例。
3. @Inject
- 作用:
- 标记构造函数、字段或方法,表示需要注入依赖。
- 在构造函数上使用
@Inject
时,Dagger 2 会自动创建该类的实例并注入所需的依赖。
- 实现原理:
- Dagger 2 在编译时会扫描所有
@Inject
注解的字段、构造函数或方法。 - 对于构造函数注入,Dagger 2 会生成一个工厂类(Factory),用于创建该类的实例。
- 对于字段或方法注入,Dagger 2 会生成代码,在运行时将依赖注入到目标类中。
- Dagger 2 在编译时会扫描所有
- 使用场景:
- 构造函数注入:用于标记需要注入依赖的构造函数。
- 字段注入:用于标记需要注入依赖的字段。
- 方法注入:用于标记需要注入依赖的方法。
- 示例:
public class UserService { private final Retrofit retrofit; @Inject public UserService(Retrofit retrofit) { this.retrofit = retrofit; } }
- 编译后,Dagger 2 会生成一个
UserService_Factory
类,用于创建UserService
实例。
4. @Component
- 作用:定义一个依赖注入的桥梁,将
@Module
提供的依赖注入到目标类中。 - 实现原理:
- Dagger 2 在编译时会扫描所有
@Component
注解的接口或抽象类。 - 根据
@Component
的配置(如modules
和dependencies
),Dagger 2 会生成一个实现类(以Dagger
为前缀)。 - 生成的实现类会集成所有
@Module
和@Provides
生成的工厂类,并在运行时完成依赖注入。
- Dagger 2 在编译时会扫描所有
- 使用场景:通过
@Component
接口或抽象类,指定需要注入的模块(@Module
)和目标类。 - 示例:
@Component(modules = {NetworkModule.class}) public interface AppComponent { void inject(MainActivity activity); }
- 编译后,Dagger 2 会生成一个
DaggerAppComponent
类,用于将NetworkModule
提供的依赖注入到MainActivity
中。 - 注意:
@Component
可以依赖其他@Component
。- 通过
@Component
生成的类会以Dagger
为前缀,例如DaggerAppComponent
。
2.作用域(Scope)机制
1. 作用域的基本概念
- 作用域:作用域是一个注解,用于标记某个依赖对象在特定范围内的生命周期。
- 单例性:在同一个作用域内,依赖对象是单例的,即每次请求都会返回同一个实例。
- 作用域链:Dagger 2 支持嵌套作用域,子作用域可以访问父作用域的依赖,但父作用域不能访问子作用域的依赖。
2. 常用的作用域注解
@Singleton
:默认的全局作用域,表示依赖对象在整个应用生命周期内是单例的。- 自定义作用域:可以根据需要定义自定义作用域,例如
@ActivityScope
、@FragmentScope
等。
3. 作用域的实现原理
- 作用域与
@Component
的绑定:作用域必须与@Component
绑定,@Component
的作用域决定了其提供的依赖对象的生命周期。 - 作用域与
@Provides
的绑定:在@Module
中,使用作用域注解标记@Provides
方法,表示该方法提供的依赖对象在特定作用域内是单例的。 - 作用域与依赖图:Dagger 2 在编译时会根据作用域生成依赖图,确保在同一个作用域内,依赖对象是单例的。
4. 作用域的使用示例
示例 1:全局单例作用域(@Singleton
)
@Module
public class AppModule {
@Provides
@Singleton
ApiService provideApiService() {
return new ApiService();
}
}
@Component(modules = {AppModule.class})
@Singleton
public interface AppComponent {
ApiService getApiService();
}
- 解释:
@Singleton
注解标记了ApiService
的作用域为全局单例。AppComponent
的作用域为@Singleton
,因此ApiService
在整个应用生命周期内是单例的。
示例 2:自定义作用域(@ActivityScope
)
@Scope
@Retention(RetentionPolicy.RUNTIME)
public @interface ActivityScope {}
@Module
public class ActivityModule {
@Provides
@ActivityScope
UserManager provideUserManager() {
return new UserManager();
}
}
@Component(modules = {ActivityModule.class}, dependencies = {AppComponent.class})
@ActivityScope
public interface ActivityComponent {
UserManager getUserManager();
}
- 解释:
@ActivityScope
是一个自定义作用域,表示UserManager
在ActivityComponent
的作用域内是单例的。- 每次创建
ActivityComponent
时,UserManager
都是一个新的实例,但在同一个ActivityComponent
内是单例的。
5. 作用域链与嵌套作用域
- 作用域链:Dagger 2 支持嵌套作用域,子作用域可以访问父作用域的依赖,但父作用域不能访问子作用域的依赖。
- 示例:
@Component(modules = {ActivityModule.class}, dependencies = {AppComponent.class}) @ActivityScope public interface ActivityComponent { UserManager getUserManager(); ApiService getApiService(); // 从父作用域(AppComponent)获取 }
- 解释:
ActivityComponent
是AppComponent
的子作用域,因此可以访问AppComponent
提供的依赖(如ApiService
)。AppComponent
不能访问ActivityComponent
提供的依赖(如UserManager
)。
3.依赖图(Dependency Graph)原理
1. 依赖图的基本概念
- 节点:依赖图中的每个节点代表一个依赖对象(如类实例、接口实现等)。
- 边:依赖图中的边表示依赖关系,例如类 A 依赖类 B,则在 A 和 B 之间会有一条边。
- 工厂类:每个节点对应一个工厂类(Factory),用于在运行时创建依赖对象。
2. 依赖图的构建过程
Dagger 2 在编译时通过注解处理器扫描代码,根据 @Module
、@Provides
、@Inject
和 @Component
等注解生成依赖图。具体步骤如下:
2.1 扫描注解
- 扫描所有
@Module
类,提取其中的@Provides
方法。 - 扫描所有
@Inject
注解,包括构造函数、字段和方法。 - 扫描所有
@Component
接口或抽象类,提取其依赖的@Module
和其他@Component
。
2.2 生成工厂类
- 为每个
@Provides
方法和@Inject
构造函数生成一个工厂类(Factory)。 - 工厂类实现了
Provider<T>
接口,用于在运行时创建依赖对象。
2.3 构建依赖图
- 根据
@Component
的配置,将@Module
和@Inject
生成的工厂类组织成一个依赖图。 - 依赖图描述了各个依赖对象之间的关系,例如类 A 依赖类 B,类 B 依赖类 C。
2.4 生成组件类
- 根据依赖图,生成
@Component
的实现类(以Dagger
为前缀)。 - 组件类负责在运行时根据依赖图创建和注入依赖对象。
3. 依赖图的核心原理
3.1 依赖解析
- 当需要注入某个依赖时,Dagger 2 会从依赖图中查找该依赖的工厂类。
- 如果依赖对象还依赖其他对象,Dagger 2 会递归解析这些依赖,直到所有依赖都被满足。
3.2 单例性
- 如果依赖对象的作用域是单例的(如
@Singleton
),Dagger 2 会在依赖图中缓存该对象,确保每次请求都返回同一个实例。 - 如果依赖对象没有作用域,Dagger 2 会每次请求时都创建一个新的实例。
3.3 作用域链
- 如果
@Component
依赖其他@Component
,Dagger 2 会将它们的依赖图合并,形成作用域链。 - 子作用域可以访问父作用域的依赖,但父作用域不能访问子作用域的依赖。
4. 依赖图的示例
假设有以下代码:
@Module
public class NetworkModule {
@Provides
OkHttpClient provideOkHttpClient() {
return new OkHttpClient();
}
@Provides
Retrofit provideRetrofit(OkHttpClient client) {
return new Retrofit.Builder()
.baseUrl("https://api.example.com")
.client(client)
.build();
}
}
@Component(modules = {NetworkModule.class})
public interface AppComponent {
Retrofit getRetrofit();
}
Dagger 2 的依赖图构建过程:
- 扫描
NetworkModule
,生成两个工厂类:NetworkModule_ProvideOkHttpClientFactory
:用于创建OkHttpClient
。NetworkModule_ProvideRetrofitFactory
:用于创建Retrofit
。
- 构建依赖图:
Retrofit
依赖OkHttpClient
。OkHttpClient
不依赖其他对象。
- 生成
AppComponent
的实现类DaggerAppComponent
,集成上述工厂类。
5. 依赖图的运行时行为
在运行时,Dagger 2 会根据依赖图完成依赖注入。例如:
AppComponent component = DaggerAppComponent.create();
Retrofit retrofit = component.getRetrofit();
- 调用
component.getRetrofit()
时,Dagger 2 会从依赖图中查找Retrofit
的工厂类。 - 发现
Retrofit
依赖OkHttpClient
,于是先调用OkHttpClient
的工厂类创建OkHttpClient
实例。 - 最后调用
Retrofit
的工厂类,传入OkHttpClient
实例,创建Retrofit
实例并返回。
6. 依赖图的优点
- 编译时验证:依赖图在编译时生成,因此可以提前发现依赖注入的问题,避免运行时错误。
- 高效性:依赖图的解析和注入在编译时已经确定,运行时不需要使用反射,性能高效。
- 灵活性:通过
@Module
和@Component
的配置,可以灵活地管理依赖关系。
7. 依赖图的注意事项
- 循环依赖:如果依赖图中存在循环依赖,Dagger 2 会抛出编译时错误。
- 作用域滥用:过度使用作用域会导致依赖图复杂化,可能引发内存泄漏或不必要的资源占用。
- 依赖冲突:如果同一个类型有多个依赖提供者,需要使用
@Qualifier
注解来区分。
核心原理总结
- 编译时生成代码:Dagger 2 在编译时通过注解处理器(APT)扫描代码,生成工厂类(Factory)和组件类(Component)。
- 依赖图的构建:Dagger 2 会根据
@Module
和@Provides
生成依赖图,描述各个依赖之间的关系。 - 运行时注入:在运行时,Dagger 2 通过生成的工厂类和组件类,按照依赖图创建实例并完成注入。
- 高效和类型安全:由于依赖注入的逻辑在编译时已经确定,Dagger 2 在运行时不会使用反射,因此性能高效且类型安全。
三、进阶实战
1. 全局组件配置
1.1 在 Application 类初始化 AppComponent
- 在
Application
类中初始化全局的AppComponent
,并将其存储为单例,供整个应用使用。 - 示例:
public class MyApplication extends Application { private AppComponent appComponent; @Override public void onCreate() { super.onCreate(); appComponent = DaggerAppComponent.builder() .appModule(new AppModule(this)) .build(); } public AppComponent getAppComponent() { return appComponent; } }
1.2 通过 AndroidInjection 自动注入
- 使用 Dagger 2 的
AndroidInjection
自动注入Activity
、Fragment
等 Android 组件。 - 示例:
public class MainActivity extends AppCompatActivity { @Inject ApiService apiService; @Override protected void onCreate(Bundle savedInstanceState) { AndroidInjection.inject(this); super.onCreate(savedInstanceState); // 使用 apiService } }
- 在
Application
中设置AndroidInjection
:public void onCreate() { super.onCreate(); DaggerAppComponent.builder() .application(this) .build() .inject(this); }
2. 多模块工程结构
2.1 按功能模块拆分 Component
- 将应用按功能模块拆分为多个
Component
,例如AppComponent
、FeatureComponent
等。 - 示例:
@Component(modules = {AppModule.class}) @Singleton public interface AppComponent { FeatureComponent featureComponent(); }
2.2 使用 @Subcomponent 实现层级依赖
- 使用
@Subcomponent
实现Component
的层级依赖,子组件可以访问父组件的依赖。 - 示例:
@Subcomponent(modules = {FeatureModule.class}) public interface FeatureComponent { void inject(FeatureActivity activity); }
- 在父组件中声明子组件:
@Component(modules = {AppModule.class}) @Singleton public interface AppComponent { FeatureComponent featureComponent(FeatureModule module); }
3. 动态参数注入
3.1 通过 @BindsInstance 传递运行时参数
- 使用
@BindsInstance
在Component
构建时传递运行时参数。 - 示例:
@Component(modules = {AppModule.class}) @Singleton public interface AppComponent { @Component.Builder interface Builder { @BindsInstance Builder application(Application application); AppComponent build(); } }
3.2 构建动态 Module(如不同环境的基础 URL)
- 通过动态
Module
传递运行时参数,例如不同环境的基础 URL。 - 示例:
@Module public class NetworkModule { private final String baseUrl; public NetworkModule(String baseUrl) { this.baseUrl = baseUrl; } @Provides Retrofit provideRetrofit(OkHttpClient client) { return new Retrofit.Builder() .baseUrl(baseUrl) .client(client) .build(); } }
- 在
Component
中动态传递参数:AppComponent appComponent = DaggerAppComponent.builder() .networkModule(new NetworkModule("https://api.example.com")) .build();
4. 完整示例
4.1 Application 类
public class MyApplication extends Application {
private AppComponent appComponent;
@Override
public void onCreate() {
super.onCreate();
appComponent = DaggerAppComponent.builder()
.application(this)
.networkModule(new NetworkModule("https://api.example.com"))
.build();
}
public AppComponent getAppComponent() {
return appComponent;
}
}
4.2 AppComponent
@Component(modules = {AppModule.class, NetworkModule.class})
@Singleton
public interface AppComponent {
void inject(MainActivity activity);
@Component.Builder
interface Builder {
@BindsInstance
Builder application(Application application);
Builder networkModule(NetworkModule networkModule);
AppComponent build();
}
}
4.3 NetworkModule
@Module
public class NetworkModule {
private final String baseUrl;
public NetworkModule(String baseUrl) {
this.baseUrl = baseUrl;
}
@Provides
Retrofit provideRetrofit(OkHttpClient client) {
return new Retrofit.Builder()
.baseUrl(baseUrl)
.client(client)
.build();
}
}
4.4 MainActivity
public class MainActivity extends AppCompatActivity {
@Inject
Retrofit retrofit;
@Override
protected void onCreate(Bundle savedInstanceState) {
AndroidInjection.inject(this);
super.onCreate(savedInstanceState);
// 使用 retrofit
}
}
写在后面
写完这篇文章的感受是:
1.大模型极大的缩短了打字和排版的时间
2.大模型比我自己写,用词更加简洁、精准,内容更加全面
If you like this article, it is written by Johnny Deng.
If not, I don’t know who wrote it.