Dagger2 的使用与基本原理
使用 Dagger2 的最佳做法:如果有可能,通过@Inject进行构造函数注入,以向 Dagger 图中添加类型。使用@Binds告知 Dagger 接口应采用哪种实现使用@Provides告知 Dagger 如何提供你的项目所不具备的类只能在组件中声明一次模块根据注释的使用生命周期,为作用域注释命名,例如和依赖注入。
严格来讲,Dagger2 并不是 Jetpack 中的一员,学习 Dagger2 的使用方法和简单原理是为了更好的掌握基于 Dagger2 封装的 Hilt。
1、理论知识
1.1 依赖注入
既然我们说了 Dagger2 是一个依赖注入
框架,那么还是需要了解下,什么是依赖注入,为什么要用依赖注入。
类通常需要引用其他类。例如,Car 类可能需要引用 Engine 类,像 Engine 这种被依赖的类称为依赖项
。一个类可以通过三种方式获取依赖项的对象:
- 类构造其所需的依赖项。例如在 Car 类中创建并初始化自己所需的 Engine 实例
- 从其他地方抓取。如 Android Context 中的 getSystemService()
- 以参数形式提供。应用可以在构造一个类时提供这些依赖项,或者将这些依赖项传入需要依赖项的函数,如在创建 Car 对象时将 Engine 对象通过 Car 的构造方法或 setter 方法传入
第三种方式就是依赖注入(DI,Dependency Injection)
。使用依赖注入时,一个类不必再自己获取依赖项,而是由外部提供。比如说:
/**
* 代码示例1:
* 在 Car 类内部,由 Car 自己创建依赖项 Engine 的实例
*/
class Car {
private Engine engine = new Engine();
public void start() {
engine.start();
}
}
class MyApp {
public static void main(String[] args) {
Car car = new Car();
car.start();
}
}
按照上述代码,Car 对 Engine 是一个强依赖关系,不利于扩展和测试,在使用依赖注入的方式后:
/**
* 示例代码2:
* 使用依赖注入方式创建对象
*/
class Car {
private final Engine engine;
// 依赖注入方式一:构造方法注入
public Car(Engine engine) {
this.engine = engine;
}
public void start() {
engine.start();
}
// 依赖注入方式二:字段(setter方法)注入
public void setEngine(Engine engine) {
this.engine = engine;
}
}
class MyApp {
public static void main(String[] args) {
// 依赖注入框架实际上干的就是创建对象并交给需要它的类这件事
Engine engine = new Engine();
Car car = new Car(engine);
car.start();
}
}
实现了控制反转(IoC,Inversion of Control)
,将原本的正向控制,即在 Car 类中主动去获取/创建所需要的外部资源 Engine,改成了被动等待外部获取到一个 Engine 的实例然后注入到 Car 中。这样做的好处有:
- 重用类以及分离依赖项:更容易换掉依赖项的实现。由于控制反转,代码重用得以改进,并且类不再控制其依赖项的创建方式,而是支持任何配置
- 易于重构:依赖项在创建对象时或编译时可以进行检查,而不是作为实现详情隐藏
- 易于测试:类不管理其依赖项,因此在测试时,可以传入不同的实现以测试不同用例
1.2 初识 Dagger2
Dagger2 可以使得项目的修改与重构更方便,Google App Store 中 Top 10000 的应用中,有 74% 使用了 Dagger:
Dagger2 在编译时会自动生成代码,这些生成的代码与原本需要手动编写的注入代码类似,这样就无需我们再手动编写冗长乏味又容易出错的异步代码了。它会完成以下工作:
- 构建并验证依赖关系图,确保每个对象的依赖关系都能满足且图中不存在依赖循环,简言之就是根据依赖关系生成有向无环图
- 为有向无环图中的类创建工厂以满足依赖关系,并通过该工厂生产实际对象
- 通过作用域管理对象的生命周期,并且决定是重复使用依赖项还是创建新的实例
- 为特定流程创建容器,这样在对象的生命周期结束后,可以及时释放内存中不再需要的对象,提升性能
在 Dagger2 中有很多注解,它们扮演了非常重要的角色,在介绍它们的用法之前,先来认识一下它们:
注解 | 描述 |
---|---|
@Inject | 指示 Dagger 如何实例化一个对象,可以作用在构造方法或需要注入的字段上 |
@Module + @Provides | 指示 Dagger 以非构造方法的方式实例化一个对象 |
@Singleton / @Scope | 作用域,用来标记依赖项的生命周期(作用域),还有一个作用是标记单例 |
@Component | 组件,创建一个 Dagger 容器,作为获取依赖项的入口 |
@Subcomponent | 子组件,用来定义更加细致的作用域 |
下面就开始逐一介绍上述注解的使用方法,在每一节介绍完使用方法后,我们会稍微看一下 Dagger2 通过 APT 生成的代码,了解一下内部实现以便更好的使用和理解 Dagger2 框架,如果你觉得篇幅过长阅读体验不佳,可以略过实现细节部分,只看每一小节前面的使用方法即可。
2、基础使用
添加依赖:
dependencies {
implementation 'com.google.dagger:dagger:2.x'
annotationProcessor 'com.google.dagger:dagger-compiler:2.x'
}
2.1 @Inject + @Component
@Inject 的作用是告诉 Dagger2 框架如何创建一个对象(其实是提供一个创建某种类型对象的方法),@Component 则会创建一个容器,Dagger2 会自动为 @Component 修饰的接口创建一个实现类,作为获取注入对象的入口。@Inject 与 @Component 配合即可实现最简单的注入,有两种注入方式,分别是构造方法注入和字段注入。
2.1.1 构造方法注入
使用方法
假设现有依赖关系如下图:
UserRepository 对象的创建依赖于 UserLocalDataSource 和 UserRemoteDataSource:
public class UserRepository {
private final UserLocalDataSource userLocalDataSource;
private final UserRemoteDataSource userRemoteDataSource;
public UserRepository(UserLocalDataSource userLocalDataSource, UserRemoteDataSource userRemoteDataSource) {
this.userLocalDataSource = userLocalDataSource;
this.userRemoteDataSource = userRemoteDataSource;
}
...
}
需要对有向无环图中的所有类的构造方法都添加 @Inject 注解以生成对应的工厂类。首先是 UserRepository 的构造方法:
public class UserRepository {
private final UserLocalDataSource userLocalDataSource;
private final UserRemoteDataSource userRemoteDataSource;
@Inject
public UserRepository(UserLocalDataSource userLocalDataSource, UserRemoteDataSource userRemoteDataSource) {
this.userLocalDataSource = userLocalDataSource;
this.userRemoteDataSource = userRemoteDataSource;
}
}
这样 Dagger2 就知道了两件事:
- 如何使用被 @Inject 注解的构造方法创建 UserRepository 对象
- UserRepository 的依赖项为 UserLocalDataSource 和 UserRemoteDataSource
此时 Dagger2 还不知道如何创建 UserRepository 的两个依赖项,所以类似的也使用 @Inject 标记依赖项的构造方法:
public class UserLocalDataSource {
@Inject
public UserLocalDataSource() { }
}
--------------------------------------------
public class UserRemoteDataSource {
@Inject
public UserRemoteDataSource() { }
}
仅添加 @Inject 还不能完成注入,还需要用 @Component 注解修饰一个接口,并在接口内定义获取指定对象(即 UserRepository)的方法。这样 @Component 会让 Dagger2 生成一个容器,容器中包含满足所提供类型所需要的所有依赖项,这就是Dagger 组件
,组件中包含一个有向无环图,告知如何提供一个对象及其依赖项:
@Component
public interface ApplicationComponent {
UserRepository userRepository();
}
此时编译项目让 APT 生成代码,会生成 ApplicationComponent 的实现类 DaggerApplicationComponent,我们通过实现类的 create() 拿到实例再调用接口方法 userRepository() 即可拿到 UserRepository 对象:
ApplicationComponent component = DaggerApplicationComponent.create();
UserRepository userRepository1 = component.userRepository();
实现细节
我们来看一下 Dagger2 使用 APT 生成的代码是什么样子的。首先代码生成的路径是:build/generated/ap_generated_sources/[release/debug]/out/[package name](使用的 Dagger2 版本不同,或者 AGP 版本不同可能会让路径也有细微差异),生成的文件有 4 个,除了 @Component 的接口实现类 DaggerApplicationComponent 之外,其余 3 个都是被 @Inject 标记了构造方法的类对应的工厂类,我们先来看 UserRepository 的工厂类:
public final class UserRepository_Factory implements Factory<UserRepository> {
// 持有两个依赖项的 Provider
private final Provider<UserLocalDataSource> userLocalDataSourceProvider;
private final Provider<UserRemoteDataSource> userRemoteDataSourceProvider;
public UserRepository_Factory(Provider<UserLocalDataSource> userLocalDataSourceProvider,
Provider<UserRemoteDataSource> userRemoteDataSourceProvider) {
this.userLocalDataSourceProvider = userLocalDataSourceProvider;
this.userRemoteDataSourceProvider = userRemoteDataSourceProvider;
}
// 通过构造方法生成 UserRepository 对象,依赖项通过对应的 Provider 的 get() 获取
@Override
public UserRepository get() {
return new UserRepository(userLocalDataSourceProvider.get(), userRemoteDataSourceProvider.get());
}
public static UserRepository_Factory create(
Provider<UserLocalDataSource> userLocalDataSourceProvider,
Provider<UserRemoteDataSource> userRemoteDataSourceProvider) {
return new UserRepository_Factory(userLocalDataSourceProvider, userRemoteDataSourceProvider);
}
public static UserRepository newInstance(UserLocalDataSource userLocalDataSource,
UserRemoteDataSource userRemoteDataSource) {
// 通过构造方法生成 UserRepository 对象,依赖项通过方法参数获取
return new UserRepository(userLocalDataSource, userRemoteDataSource);
}
}
可以看到工厂类实现了 Factory 接口,且持有依赖项的 Provider 对象,这里 Factory 其实是继承自 Provider 的:
public interface Factory<T> extends Provider<T> {
}
---------------------------------------------------
public interface Provider<T> {
T get();
}
而 get() 实际上就是获取指定类的对象并向外提供,获取方式正是 @Inject 标记的方法创建对象的方式。至于两个依赖项的工厂,也是大同小异,仅贴出一个:
public final class UserLocalDataSource_Factory implements Factory<UserLocalDataSource> {
private static final UserLocalDataSource_Factory INSTANCE = new UserLocalDataSource_Factory();
// 生产 UserLocalDataSource 对象的方法,生产方式是 @Inject 指定的,通过
// UserLocalDataSource 的构造方法
@Override
public UserLocalDataSource get() {
return new UserLocalDataSource();
}
public static UserLocalDataSource_Factory create() {
return INSTANCE;
}
public static UserLocalDataSource newInstance() {
return new UserLocalDataSource();
}
}
总结一下 @Inject 注解生成的工厂类:
- 内部持有依赖项的 Provider 对象来维护依赖关系
- 通过 get() 获取指定类的对象后向外提供该对象,获取对象的方式是由 @Inject 标记的方法决定的
再来看由 @Component 生成的接口实现类:
public final class DaggerApplicationComponent implements ApplicationComponent {
private DaggerApplicationComponent() {
}
public static Builder builder() {
return new Builder();
}
// 调用构建者的 build() 得到 ApplicationComponent 的实现类对象
public static ApplicationComponent create() {
return new Builder().build();
}
// 接口内方法的实现:调用 UserRepository 的构造方法返回一个实例
@Override
public UserRepository userRepository() {
return new UserRepository(new UserLocalDataSource(), new UserRemoteDataSource());}
public static final class Builder {
private Builder() {
}
public ApplicationComponent build() {
return new DaggerApplicationComponent();
}
}
}
2.1.2 字段注入
有时,构造方法注入并不能满足所有的使用场景。比如说,对于 Android 系统中的 Activity 而言,它们的实例化都是由系统完成的,其余角色无法通过调用构造方法创建一个 Activity 实例,也更不可能在构造方法上加 @Inject 注解指定如何实例化及其依赖项了:
/**
* LoginActivity 依赖 LoginViewModel,但是你不能用构造方法注入,因为
* 只有 Android 系统才能调用 Activity 的构造方法去实例化一个 Activity
*/
public class LoginActivity extends Activity {
LoginViewModel loginViewModel;
@Inject
public LoginActivity(LoginViewModel loginViewModel) {
this.loginViewModel = loginViewModel;
}
}
此时,无法通过构造方法注入,所以需要 @Inject 的第二种使用方式 —— 字段注入。
使用方法
首先,还是要用 @Inject 修饰 UserRepository、UserLocalDataSource、UserRemoteDataSource 的构造方法,这一点是没有变的,需要变化的是 @Component 修饰的接口:
@Component
public interface ApplicationComponent {
// UserRepository userRepository();
/**
* 告诉 Dagger,MainActivity 请求注入,所以依赖图必须满足所有 MainActivity 正在注入的字段的依赖关系。
* 参数不能使用多态,只能传入被注入的那个类,比如这里就不能传 Activity,因为 Dagger 不知道要提供的具体内容。
*/
void injectActivity(MainActivity mainActivity);
}
最终获取对象的方式也有所改变:
public class MainActivity extends AppCompatActivity {
// 用 @Inject 标记被注入的字段,表示需要 Dagger2 从依赖图中提供一个 LoginViewModel 的实例
@Inject
UserRepository userRepository;
@Override
protected void onCreate(Bundle savedInstanceState) {
// 调用接口内定义的注入方法
DaggerApplicationComponent.create().injectActivity(this);
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
}
需要注意的使用细节:
- 使用 Activity 时,应该在 onCreate() 的 super.onCreate() 之前注入 Dagger,因为在 super.onCreate() 的恢复阶段,Activity 会附加可能需要访问 Activity 绑定的 Fragment
- 使用 Fragment 时,应在 onAttach() 中注入 Dagger,在 super.onAttach() 的前后都可
实现细节
字段注入修改了接口内方法的定义方式,所以实现类也跟随发生了变化:
public final class DaggerApplicationComponent implements ApplicationComponent {
...
private UserRepository getUserRepository() {
return new UserRepository(new UserLocalDataSource(), new UserRemoteDataSource());}
// 实现接口方法
@Override
public void injectActivity(MainActivity mainActivity) {
injectMainActivity(mainActivity);}
private MainActivity injectMainActivity(MainActivity instance) {
// 将需要注入的 Activity 和对象交给 MainActivity_MembersInjector
MainActivity_MembersInjector.injectUserRepository(instance, getUserRepository());
return instance;
}
}
我们看到执行字段注入的工作实际上是由 MainActivity_MembersInjector 来完成的:
public final class MainActivity_MembersInjector implements MembersInjector<MainActivity> {
// 持有所需类的 Provider 对象
private final Provider<UserRepository> userRepositoryProvider;
public MainActivity_MembersInjector(Provider<UserRepository> userRepositoryProvider) {
this.userRepositoryProvider = userRepositoryProvider;
}
public static MembersInjector<MainActivity> create(
Provider<UserRepository> userRepositoryProvider) {
return new MainActivity_MembersInjector(userRepositoryProvider);}
@Override
public void injectMembers(MainActivity instance) {
injectUserRepository(instance, userRepositoryProvider.get());
}
// 将 userRepository 赋值给 MainActivity 中的 userRepository 字段(不能是 private 的)
public static void injectUserRepository(MainActivity instance, UserRepository userRepository) {
instance.userRepository = userRepository;
}
}
整个流程也很简单:
- 定义接口方法时将需要注入的 Activity 填写到方法参数上
- 接口实现类能根据依赖关系生成一个带注入类的对象,并将其与待注入的 Activity 一起传给 MembersInjector
- MembersInjector 将对象赋值给 Activity 中对应字段
2.2 @Module + @Provides
即便 @Inject 提供了两种注入方式,但仍不能满足我们所有的使用场景,比如你的依赖项中包含一个第三方框架提供的对象,你无法修改该框架的代码,去该对象所在类的构造方法上加一个 @Inject,或者该对象根本就不是通过构造方法创建对象的,这时候 @Module 就派上用场了。
2.2.1 使用方法
@Module 用来修饰一个类,这个类就是一个 Dagger 模块
,模块内可以定义多个被 @Provides 修饰的返回值类型为 T 的方法,用来告诉 Dagger 如何创建一个 T 类型的对象。比如说我们在 NetworkModule 中定义一个通过 Retrofit 创建 LoginRetrofitService 对象的方法:
@Module
public class NetworkModule {
// @Provides 告诉 Dagger2 如何提供项目中所不具备的类(第三方库中的类)对象,
// 即创建一个该方法返回值类型的实例,方法参数是该类型的依赖
@Provides
public LoginService provideLoginService(OkHttpClient okHttpClient) {
// 当 Dagger 需要提供 LoginService 类型的实例时,就会运行到 @Provides 方法内的代码
return new Retrofit.Builder()
.client(okHttpClient)
.baseUrl("https://example.com")
.build()
.create(LoginService.class);
}
// provideLoginService() 依赖 OkHttpClient,需要提供一下创建方法
@Provides
public OkHttpClient provideOkHttpClient() {
return new OkHttpClient.Builder()
.build();
}
}
为了将模块中的依赖关系添加到 Dagger2 的有向无环图中,需要将其添加到 @Component 中:
// modules 可以指定多个
@Component(modules = NetworkModule.class)
public interface ApplicationComponent {
void injectActivity(MainActivity mainActivity);
}
最后还是用字段注入方式注入 LoginService:
public class MainActivity extends AppCompatActivity {
@Inject
LoginService loginService;
@Override
protected void onCreate(Bundle savedInstanceState) {
DaggerApplicationComponent.create().injectActivity(this);
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
}
2.2.2 实现细节
使用了 @Module + @Provides 组合后,Dagger2 会把 Module 中被 @Provides 修饰的方法的返回值类型添加到描述依赖关系的有向无环图中,方式与 @Inject 一样,都是生成该类型对应的 Factory 实现类。对于本例来说,NetworkModule 中定义的两个 @Provides 方法分别返回 LoginService、OkHttpClient 类型,所以 Dagger2 会为它们生成两个 Factory 实现类,下面我们只看有依赖项的 NetworkModule_ProvideLoginServiceFactory:
public final class NetworkModule_ProvideLoginServiceFactory implements Factory<LoginService> {
// 持有所在的 Module 对象
private final NetworkModule module;
// 持有依赖项的 Provider
private final Provider<OkHttpClient> okHttpClientProvider;
public NetworkModule_ProvideLoginServiceFactory(NetworkModule module,
Provider<OkHttpClient> okHttpClientProvider) {
this.module = module;
this.okHttpClientProvider = okHttpClientProvider;
}
// 调用依赖项 Provider 的 get() 获取依赖项对象,然后 module 中定义的获取
// LoginService 对象的 provideLoginService() 方法
@Override
public LoginService get() {
return provideLoginService(module, okHttpClientProvider.get());
}
public static NetworkModule_ProvideLoginServiceFactory create(NetworkModule module,
Provider<OkHttpClient> okHttpClientProvider) {
return new NetworkModule_ProvideLoginServiceFactory(module, okHttpClientProvider);
}
public static LoginService provideLoginService(NetworkModule instance,
OkHttpClient okHttpClient) {
return Preconditions.checkNotNull(instance.provideLoginService(okHttpClient), "Cannot return null from a non-@Nullable @Provides method");
}
}
看到这里其实更加印证了,所有处于有向无环图中的类都是通过对应的工厂类维持依赖关系的。
2.2.3 Module 传参
如果在使用 Module 创建对象时,需要外部传入该如何处理呢?最基础的方法是将参数定义成 Module 的成员变量并通过构造方法接收外部传值。比如说创建 OkHttpClient 对象时,由外部传入读取数据的超时时间:
@Module
public class NetworkModule {
private int readTime;
private TimeUnit timeUnit;
public NetworkModule(int readTime, TimeUnit timeUnit) {
this.readTime = readTime;
this.timeUnit = timeUnit;
}
@Singleton
@Provides
public OkHttpClient provideOkHttpClient() {
return new OkHttpClient.Builder()
.readTimeout(readTime, timeUnit)
.build();
}
...
}
然后在用 Component 的接口实现类执行注入时,手动配置 NetworkModule 对象,传入所需参数:
DaggerApplicationComponent.builder()
.networkModule(new NetworkModule(10, TimeUnit.SECONDS))
.build()
.injectActivity(this);
这种方法虽然很容易理解,但是很 low。本来我们使用 Dagger2 就是为了解耦,不再 new 对象,你这现在又来个 new NetworkModule() 不是在开倒车么?可以使用 @BindInstance 注解来解决这个问题。
首先配置 Module,将需要外部传入的参数直接放在方法上:
@Module
public class NetworkModule {
@Singleton
@Provides
public OkHttpClient provideOkHttpClient(long readTime, TimeUnit timeUnit) {
return new OkHttpClient.Builder()
.readTimeout(readTime, timeUnit)
.build();
}
...
}
然后修改 Component,自定义 Component.Builder:
@Singleton
@Component(modules = NetworkModule.class)
public interface ApplicationComponent {
void injectActivity(MainActivity mainActivity);
void injectActivity(SecondActivity secondActivity);
// 自己定义 Component 内 Builder 的实现,而不用默认实现
@Component.Builder
interface Builder {
@BindsInstance
Builder initReadTime(long readTime, TimeUnit timeUnit);
@BindsInstance
Builder initReadTimeUnit(TimeUnit timeUnit);
ApplicationComponent build();
}
}
使用方式:
DaggerApplicationComponent.builder()
.initReadTime(10)
.initReadTimeUnit(TimeUnit.SECONDS)
.build()
.injectActivity(this); // Activity
但是这种方法不能解决全部问题,因为 Dagger2 规定 @BindsInstance 修饰的方法最多只能有一个参数,且多个方法之间的参数类型不能重复。
2.3 @Singleton / @Scope
2.3.1 使用方法
当你想让 Dagger2 提供一个单例类时,可以使用 @Singleton 注解。具体的规则要分情况来看:
- 当你使用 @Inject 提供对象时,@Singleton 需要修饰对象所在的类,还需要修饰 @Component 标记的接口
- 如果是像 Retrofit 或 OkHttp 这种第三方框架的类,不能用 @Inject 注入而使用 @Module + @Provides 提供对象时,需要用 @Singleton 修饰 @Provides 标记的方法以及使用了该方法所在模块的 @Component 组件接口
先看第一种情况,在未使用单例时,测试代码如下:
public class MainActivity extends AppCompatActivity {
@Inject
UserRepository userRepository1;
@Inject
UserRepository userRepository2;
@Override
protected void onCreate(Bundle savedInstanceState) {
DaggerApplicationComponent.create().injectActivity(this);
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Log.d("Test", "result:" + (userRepository1 == userRepository2));
}
}
此时输出结果为 false 说明 userRepository1 和 userRepository2 不是同一个对象。然后我们按照前面给出的规则给 UserRepository 类和 ApplicationComponent 接口加上 @Singleton:
@Singleton
public class UserRepository {
private final UserLocalDataSource userLocalDataSource;
private final UserRemoteDataSource userRemoteDataSource;
@Inject
public UserRepository(UserLocalDataSource userLocalDataSource, UserRemoteDataSource userRemoteDataSource) {
this.userLocalDataSource = userLocalDataSource;
this.userRemoteDataSource = userRemoteDataSource;
}
}
------------------------------------------------------------
@Singleton
@Component(modules = NetworkModule.class)
public interface ApplicationComponent {
void injectActivity(MainActivity mainActivity);
}
再次运行 MainActivity 比较结果就为 true 了,也就是单例配置生效了。
下面再来看第二种情况,通过模块构造单例时,需要给提供对象的 @Provides 方法、使用了该方法所在的模块类的组件上都添加 @Singleton:
@Module
public class NetworkModule {
@Singleton
@Provides
public LoginService provideLoginService(OkHttpClient okHttpClient) {
// 当 Dagger 需要提供 LoginService 类型的实例时,就会运行到 @Provides 方法内的代码
return new Retrofit.Builder()
.client(okHttpClient)
.baseUrl("https://example.com")
.build()
.create(LoginService.class);
}
@Singleton
@Provides
public OkHttpClient provideOkHttpClient() {
return new OkHttpClient.Builder()
.build();
}
}
------------------------------------------------------------
@Singleton
@Component(modules = NetworkModule.class)
public interface ApplicationComponent {
void injectActivity(MainActivity mainActivity);
}
@Singleton 是 javax.inject 包中提供的唯一一个作用域注解,它本质上是一个 @Scope,用来描述对象的作用域。关于 @Scope 的具体介绍在 3.3 节中。
2.3.2 实现细节
上一小节给 UserRepository、LoginService 和 OkHttpClient 这三个类配置成单例之后,与未配置 @Singleton 时相比,各个类的 Factory 内部没有变化,变化发生在 @Component 接口的实现类上:
public final class DaggerApplicationComponent implements ApplicationComponent {
// 原本只持有 ApplicationComponent 上配置的模块 NetworkModule 的引用,
// 现在变成三个单例类的 Provider 对象了
private Provider<UserRepository> userRepositoryProvider;
private Provider<OkHttpClient> provideOkHttpClientProvider;
private Provider<LoginService> provideLoginServiceProvider;
// 在非单例模式下,构造方法是空的
private DaggerApplicationComponent(NetworkModule networkModuleParam) {
initialize(networkModuleParam);
}
// 在非单例模式下,没有 initialize()
@SuppressWarnings("unchecked")
private void initialize(final NetworkModule networkModuleParam) {
// DoubleCheck.provider() 包裹的其实就是各个类的工厂对象
this.userRepositoryProvider = DoubleCheck.provider(UserRepository_Factory.create(UserLocalDataSource_Factory.create(), UserRemoteDataSource_Factory.create()));
this.provideOkHttpClientProvider = DoubleCheck.provider(NetworkModule_ProvideOkHttpClientFactory.create(networkModuleParam));
this.provideLoginServiceProvider = DoubleCheck.provider(NetworkModule_ProvideLoginServiceFactory.create(networkModuleParam, provideOkHttpClientProvider));
}
private MainActivity injectMainActivity(MainActivity instance) {
// userRepositoryProvider.get() 取出的是 UserRepository 对应的工厂类对象
MainActivity_MembersInjector.injectRepository(instance, userRepositoryProvider.get());
MainActivity_MembersInjector.injectLoginService1(instance, provideLoginServiceProvider.get());
MainActivity_MembersInjector.injectLoginService2(instance, provideLoginServiceProvider.get());
return instance;
}
}
假如要提供 T 类型的单例对象,那么在 @Component 接口实现类中会做如下几件事:
- 内部会持有 Provider<T> 对象,在 initialize() 中被初始化为 DoubleCheck.provider(Factory<T>),DoubleCheck 类会保证 T 为单例
- 在向 Activity 做字段注入时,将 T 类型对应的工厂类对象取出交给待注入的 Activity
下面来看看 DoubleCheck 是如何保证单例的:
public final class DoubleCheck<T> implements Provider<T>, Lazy<T> {
/**
* Returns a {@link Provider} that caches the value from the given delegate provider.
*/
public static <P extends Provider<T>, T> Provider<T> provider(P delegate) {
checkNotNull(delegate);
// 如果 delegate 已经是一个 DoubleCheck 对象了,那就不再用 DoubleCheck 再次包装它了,直接返回
if (delegate instanceof DoubleCheck) {
/* This should be a rare case, but if we have a scoped @Binds that delegates to a scoped
* binding, we shouldn't cache the value again. */
return delegate;
}
// delegate 传的是生产对象的工厂,如 UserRepository_Factory,用 DoubleCheck 封装一层返回
return new DoubleCheck<T>(delegate);
}
}
将原本提供对象的工厂包装成一个 DoubleCheck 对象,这样调用 get() 去获取对象时,就会调用到 DoubleCheck 的 get():
private static final Object UNINITIALIZED = new Object();
private volatile Provider<T> provider;
private volatile Object instance = UNINITIALIZED;
private DoubleCheck(Provider<T> provider) {
assert provider != null;
this.provider = provider;
}
@Override
public T get() {
Object result = instance;
// 第一次调用当前 DoubleCheck 对象的 get() 会命中 if
if (result == UNINITIALIZED) {
synchronized (this) {
result = instance;
if (result == UNINITIALIZED) {
result = provider.get();
instance = reentrantCheck(instance, result);
/* Null out the reference to the provider. We are never going to need it again, so we
* can make it eligible for GC. */
provider = null;
}
}
}
return (T) result;
}
对 result 进行双重判断是否为 UNINITIALIZED,如果是,则调用 provider.get() 给 result 赋值,再用 result 给 instance 赋值,provider 就是提供对象的工厂类,其实就是将所需对象给了 result。这样在后续再次调用 get() 时就不会命中 if 条件了,直接用 instance 给 result 赋值并返回,从而保证被 DoubleCheck.provider(Factory<T>) 包裹的工厂生产出来的对象是单例。
2.3.3 全局单例与局部单例
我们以上说的单例实际上都是局部单例,单例的作用域范围只是在 @Component 接口方法指定的需要注入的 Activity 内。现在我们增加一个 Activity 对 UserRepository 执行注入:
@Singleton
@Component(modules = NetworkModule.class)
public interface ApplicationComponent {
void injectActivity(MainActivity mainActivity);
// 增加 SecondActivity,也进行注入
void injectActivity(SecondActivity secondActivity);
}
--------------------------------------------------------
// SecondActivity 类似
public class MainActivity extends AppCompatActivity {
@Inject
UserRepository repository1;
@Inject
UserRepository repository2;
@Override
protected void onCreate(Bundle savedInstanceState) {
DaggerApplicationComponent.create().injectActivity(this);
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Log.d("MainActivity", "userRepository1:" + repository1 + ",userRepository2:" + repository2);
}
}
输出结果:
能看到 UserRepository 只在 Activity 内部保持了单例,但是在 Activity 之间不是单例。这是因为在两个 Activity 中调用 DaggerApplicationComponent.create() 生成的 DaggerApplicationComponent 不是同一个对象,导致其内部的 DoubleCheck 也不是同一个对象,所以会出现,两个 DoubleCheck 之间无法保证单例,只能保证同一个 DoubleCheck 对象内的单例。
知道了原因,解决方案也就容易想到,想让 UserRepository 是全局单例,即在 Application 范围内保持单例,将 DaggerApplicationComponent 提到 Application 中就行了:
public class MyApplication extends Application {
private ApplicationComponent applicationComponent;
@Override
public void onCreate() {
super.onCreate();
applicationComponent = DaggerApplicationComponent.create();
}
public ApplicationComponent getApplicationComponent() {
return applicationComponent;
}
}
然后在 Activity 中从 MyApplication 获取 ApplicationComponent 执行注入:
((MyApplication)getApplication()).getApplicationComponent().injectActivity(this);
这样就实现了全局单例。
3、进阶使用
之前我们只引入了一个 Component,但是在实际项目中,通常是有多个 Component 的,并且由于 Dagger2 在语法上不允许多个 Component 向同一个类进行注入,所以通常都是将多个 Component 组合到一起使用:
其实用到 Component 组合的场景非常多,我们只能选择其中之一进行介绍。假如有一个全局单例的组件 ApplicationComponent 对 Activity 进行注入:
@Module
public class NetworkModule {
@Singleton
@Provides
public NetworkObject provideNetworkObject() {
return new NetworkObject();
}
}
----------------------------------------------------
@Singleton
@Component(modules = {NetworkModule.class})
public interface ApplicationComponent {
void injectActivity(TestActivity testActivity);
// 其他注入方法省略....
}
----------------------------------------------------
public class MyApplication extends Application {
private ApplicationComponent applicationComponent;
@Override
public void onCreate() {
super.onCreate();
applicationComponent = DaggerApplicationComponent.create();
}
public ApplicationComponent getApplicationComponent() {
return applicationComponent;
}
}
现在,假设我们使用 MVP 模式展示页面,那么 Presenter 也需要一个单独的 Module 和 Component:
@Module
public class PresenterModule {
@Provides
public Presenter providePresenter() {
return new Presenter();
}
}
----------------------------------------------------
@Component(modules = {PresenterModule.class})
public interface PresenterComponent {
// 这里让 PresenterComponent 也注入 TestActivity,编译会报错的
void injectActivity(TestActivity testActivity);
}
由于 ApplicationComponent 和 PresenterComponent 都要注入 TestActivity,这时编译就会报错:
PresenterComponent.java:10: 错误: [Dagger/MissingBinding] com.demo.dagger.object.NetworkObject cannot be provided without an @Inject constructor or an @Provides-annotated method.
实际上这个错误是因为有两个 Component 注入 TestActivity 导致的,那么只能将 Component 组合到一起进行注入,下面介绍两种方法。
3.1 dependencies
先确定使用哪个 Component 作为主 Component,确定后,主 Component 仍然执行注入操作,而其他 Component 作为依赖项,不再执行注入,转而提供 Module 提供的对象。一般我们会选择 ApplicationComponent 作为主 Component。
具体操作是,在 ApplicationComponent 的 @Component 注解值中增加 dependencies,指定依赖的 Component:
@Singleton
@Component(modules = {NetworkModule.class}, /*增加依赖项Component*/dependencies = {PresenterComponent.class})
public interface ApplicationComponent {
void injectActivity(TestActivity loginActivity);
// 其他注入方法省略...
}
然后将被依赖的 PresenterComponent 中的注入方法改为提供 Presenter 对象:
@Component(modules = {PresenterModule.class})
public interface PresenterComponent {
// void injectActivity(TestActivity loginActivity);
Presenter providePresenter();
}
最后在创建 ApplicationComponent 接口实例时需要指定 PresenterComponent:
public class MyApplication extends Application {
private ApplicationComponent applicationComponent;
@Override
public void onCreate() {
super.onCreate();
applicationComponent = DaggerApplicationComponent.builder()
// 指定 PresenterComponent 的实现类
.presenterComponent(DaggerPresenterComponent.create())
.build();
}
}
3.2 @SubComponent
此外还可以通过 @SubComponent 注解标记子组件解决上述问题。与 dependencies 类似,我们还是想让 ApplicationComponent 作为主 Component,所以将 @SubComponent 标记给 PresenterComponent 使其作为子组件:
@Subcomponent(modules = {PresenterModule.class})
public interface PresenterComponent {
void injectActivity(TestActivity loginActivity);
}
注意由子组件执行注入,而主组件则定义一个返回子组件对象的方法:
@Singleton
@Component(modules = {NetworkModule.class})
public interface ApplicationComponent {
// void injectActivity(TestActivity loginActivity);
PresenterComponent getPresenterComponent();
}
创建 ApplicationComponent 接口实例后,通过该实例获取 PresenterComponent 实例再执行注入:
public class MyApplication extends Application {
private ApplicationComponent applicationComponent;
@Override
public void onCreate() {
super.onCreate();
applicationComponent = DaggerApplicationComponent.create();
}
}
----------------------------------------------------
public class TestActivity extends AppCompatActivity {
@Inject
NetworkObject networkObject;
@Inject
Presenter presenter;
@Override
protected void onCreate(Bundle savedInstanceState) {
((MyApplication) getApplication()).getApplicationComponent().getPresenterComponent().injectActivity(this);
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
}
可以看到与 dependencies 方式相比,主组件与子组件在接口方法定义方式上是相反的。并且生成的代码中,子组件的实现类是主组件的 private 内部类,导致子组件的方法无法被我们调用,没有使用 dependencies 的方式灵活。
3.3 @Scope
假如现在有个需求,想让 Presenter 也变成个单例对象,按照我们之前的做法,给 PresenterModule 中的 Provides 方法和 PresenterComponent 加上 @Singleton 之后,会发现编译报错了:
com.demo.dagger.component.ApplicationComponent also has @Singleton
它说 ApplicationComponent 中已经有 @Singleton 了。显然,Dagger2 要求 @Singleton 不能用在多个组件上。实际上,是要求同一个作用域注解不能用在多个组件上。@Scope 注解表示作用域,被其修饰的类或提供对象的方法会被做成单例,所以我们可以用 @Scope 自定义一个作用域注解:
@Scope
@Retention(RetentionPolicy.RUNTIME)
public @interface ActivityScope {
}
实际上它和 @Singleton 就只差一个注解名字:
@Scope
@Documented
@Retention(RUNTIME)
public @interface Singleton {}
我们给 PresenterComponent 使用 @ActivityScope,这样编译错误就解决了:
@Module
public class PresenterModule {
@ActivityScope
@Provides
public Presenter providePresenter() {
return new Presenter();
}
}
----------------------------------------------------
@ActivityScope
@Subcomponent(modules = {PresenterModule.class})
public interface PresenterComponent {
void injectActivity(TestActivity loginActivity);
}
使用 @Scope 的原则:
- 多个 Component 上面的 Scope 不能相同
- 没有 Scope 的 Component 不能依赖有 Scope 的组件
- 使用作用域注解的模块只能在带有相同作用域注解的组件中使用
- 使用构造方法注入(通过
@Inject
)时,应在类中添加作用域注解;使用 Dagger 模块时,应在@Provides
方法中添加作用域注解
4、其他注解的使用
4.1 Named 注解
此外还有一种情况,比如有个 User 类:
public class User {
private String name;
private int pwd;
public User(String name, int pwd) {
this.name = name;
this.pwd = pwd;
}
}
在 Module 中有两个提供该类对象的方法:
@Provides
public User provideUser1() {
return new User("user1", 123);
}
@Provides
public User provideUser2() {
return new User("user2", 456);
}
那么你在注入对象时如果还是用基础方法:
@Inject
User user;
框架是无法确定使用 provideUser1() 还是 provideUser2() 注入 user 的,此时就要用到 @Named 注解,给方法和被注入对象都用 @Named 的 key 进行对应:
@Named(value = "key1")
@Provides
public User provideUser1() {
return new User("user1", 123);
}
@Named(value = "key2")
@Provides
public User provideUser2() {
return new User("user2", 456);
}
========================================
// 被注入的对象也用 @Named 并指定一个值,然后框架就会使用这个值对应的方法进行注入
@Named(value = "key1")
@Inject
User user1;
@Named(value = "key2")
@Inject
User user2;
如果 User 构造方法的参数不是写死的,而是需要以参数形式传入,可以把参数定义成 Module 中的成员变量。
@Named 注解内部使用了 @Qualifier 注解可以关注一下。
4.2 Lazy 与 Provider
如果需要实现懒加载式的注入,可以使用 Lazy 或 Provider:
@Inject
Lazy<User> lazy;
@Inject
Provider<User> provider;
User user1 = lazy.get();
User user2 = provider.get();
这样直到调用 get() 时才去注入 User 对象。Lazy 与 Provider 的区别在于,Lazy 是一个单例,而 Provider 不是:
@Override
public void injectMembers(TestActivity instance) {
if (instance == null) {
throw new NullPointerException("Cannot inject members into a null reference");
}
...
instance.lazy = DoubleCheck.lazy(aAndProviderAndLazyProvider);
instance.provider = aAndProviderAndLazyProvider;
}
DoubleCheck 包裹的对象都会处理成单例,因此 instance.lazy 是单例,而 instance.provider 只是一个普通成员,不是单例。
5、总结
使用 Dagger2 的最佳做法:
-
如果有可能,通过
@Inject
进行构造函数注入,以向 Dagger 图中添加类型。如果没有可能,则执行以下操作:- 使用
@Binds
告知 Dagger 接口应采用哪种实现 - 使用
@Provides
告知 Dagger 如何提供你的项目所不具备的类
- 使用
-
只能在组件中声明一次模块
-
根据注释的使用生命周期,为作用域注释命名,例如
@ApplicationScope
、@LoggedUserScope
和@ActivityScope
参考资料:
开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!
更多推荐
所有评论(0)