Dagger2 依赖的接力游戏(六):Bind、BindInst
本篇示例代码收录在项目的chapter6分支
我们知道Dagger2是帮我们管理依赖关系的工具,而它对依赖关系的匹配是基于Class类型、Qualifier、Scope三者综合判断的。在前面的文章中,我们声明某一个类型的需求或者提供,都是使用注解直接声明的。比如我们使用Provides注解声明一个提供类型:
@Provides
Engine getEngine(){
return new Engine(1);
}
或者在Component中声明一个需求类型:
@Component
public interface EngineComponent{
Engine getEngine();
}
Dagger都会将它们的类型当作Engine.class。除此之外,Dagger还提供了一些间接的类型声明方法,这就是本文要讲的三种注解。
Bind注解
Bind注解像一个桥梁,它仅作用于提供类型的声明。它可以将某个父类型的需求,嫁接到其子类型的实现上,从而避免子类型的暴露,使调用代码依赖于抽象类型,而不是具体的子类。我们用示例来说明这个作用。
比如我们有一个CarComponent,它提供两种不同的Car实例,getCarA用于创建Mondeo汽车,getCarB用于创建没有品牌的汽车。
@Subcomponent(modules = CarModule.class)
public interface CarComponent {
@QualifierMondeo
Car getCarA();
Car getCarB();
@Subcomponent.Builder
interface Builder{
CarComponent build();
}
}
那我们在CarModule中可以这么实现:
@Module
public abstract class CarModule {
@Binds
@QualifierMondeo
abstract Car provideCarA(Mondeo mondeo);
}
我们使用@Bind修饰,将Car类型的依赖,转接到了Mondeo类。当然Mondeo的提供类型也是要声明的,我们用构造方法来实现,并且给Mondeo一个名字和八缸引擎:
public class Mondeo extends Car {
@Inject
public Mondeo(Engine engine){
super(engine);
mName = "mondeo";
engine.mCylinderNumbers = 8;
}
}
因为我们使用了Bind注解,CarModule变成了抽象类,不能提供创建普通Car的方法了,所以我们使用Car构造方法来声明P(Car)。
public class Car {
String mName;
Engine mEngine;
@Inject
public Car(Engine engine){
mEngine = engine;
}
public String getName(){
return mName;
}
Engine getEngine(){
return mEngine;
}
}
Car依赖的Engine类型提供我们就不在解释了,默认的引擎数是1。我们用下面的代码来测试一下:
public static void main(String[] args) {
EngineComponent engineComponent = DaggerEngineComponent.builder()
.engineModule(new EngineModule()).tag("tagged success")
.build();
testCar(engineComponent);
}
private static void testCar(EngineComponent engineComponent) {
CarComponent carComponent = engineComponent.carComponent().build();
Car carA = carComponent.getCarA();
Car carB = carComponent.getCarB();
System.out.println("carA : " + carA + "; name : " + carA.getName() + "; cylinders : " + carA.getEngine().getCylinderNumbers());
System.out.println("carB : " + carB + "; name : " + carB.getName() + "; cylinders : " + carB.getEngine().getCylinderNumbers());
}
得到结果:
carA : com.example.dagger2.Mondeo@610455d6; name : mondeo; cylinders : 8
carB : com.example.dagger2.Car@511d50c0; name : null; cylinders : 1
说明我们的依赖类型嫁接成功了,getCarA最终调用的是Mondeo的构造函数。我们稍微看下源码:
private final class CarComponentImpl implements CarComponent {
private CarComponentImpl(CarComponentBuilder builder) {}
private Mondeo getMondeo() {
return new Mondeo(
EngineModule_ProvideEngineFactory.proxyProvideEngine(
DaggerEngineComponent.this.engineModule));
}
@Override
public Car getCarA() {
return getMondeo();
}
@Override
public Car getCarB() {
return new Car(
EngineModule_ProvideEngineFactory.proxyProvideEngine(
DaggerEngineComponent.this.engineModule));
}
}
可以看到依赖嫁接之后,getCarA的方法,调用的Mondeo类的构造方法,而getCarB调用的是Car构造方法。
有的同学就说了,这么写和我直接使用@Provide创建一个对象有啥区别呢?比如CarModule可以写成这样:
@Module
public class CarModule {
@Binds
@QualifierMondeo
Car provideCarA(Engine engine){
return new Mondeo(engine)
}
}
这里我把用这种方式实现的源码也贴出来,通过代码看看两者区别
@Override
public Car getCarA() {
return CarModule_ProvideCarAFactory.proxyProvideCarA(
carModule,
EngineModule_ProvideEngineFactory.proxyProvideEngine(
DaggerEngineComponent.this.engineModule));
}
@Override
public Car getCarB() {
return new Car(
EngineModule_ProvideEngineFactory.proxyProvideEngine(
DaggerEngineComponent.this.engineModule));
}
对比前面的getCarA以及这里的getCarB方法,我们发现如果使用绑定的方式来创建Mondeo,我们不需要创建CarModue实例(也不能为CarComponent创建CarModue实例),然后再通过它创建我们真正要的Mondeo,而是直接调用构造方法,这在执行效率上会更加高效一些。
当然,我觉得最大的好处就是方便,反正我的Mondeo构造方法也是要写的,再加个注解就好了,为啥这里还要再多些一遍创建实例的代码,万一参数或者返回类型有变化,修改起来也麻烦。
BindInstance注解
BindInstance注解像是一个管道,它允许我们在Component里,提前为某个类型对象打一个桩,并在构造Component实例的时候,用该类型或子类型的一个对象,替换(赋值)这个桩,以便其他地方获取。通过BindInstance打出来的桩,在构造对象的时候必须进行非空赋值,否则会导致异常。Dagger2将它设计为一个强制绑定关系,仅供必要时使用。
以下来看示例代码:
@Component(modules = {EngineModule.class})
public interface EngineComponent {
String getTag();
CarComponent.Builder carComponent();
@Component.Builder
interface Builder{
@BindsInstance
Builder tag(String tag);
Builder engineModule(EngineModule engineModule);
EngineComponent build();
}
}
在上面的EngineComponent里,我们定义了String类型的getTag方法,这个方法返回的对象,是在Builder中tag方法传递进去的。如果我们不在Builder中定义tag方法,那么Component就会在Module中进行查找,导致编译失败。所以为了定义这个tag方法,我们需要向上面一样,使用Component.Builder注解,手动实现我们的Builder类,而这本来是可以省略的。同样,如果我们在Module中有相同的提供类型,就会因类型冲突导致报重复绑定错误,所以这也是一个基于类型的强制绑定。
现在我们来测试一下效果:
public static void main(String[] args) {
EngineComponent engineComponent = DaggerEngineComponent.builder()
.engineModule(new EngineModule()).tag("tagged success")
.build();
System.out.println("EngineComponent tag : " + engineComponent.getTag());
}
查看结果:
EngineComponent tag : tagged success
Process finished with exit code 0
结果是正确的。这个注解,就是用于扩充我们Component的功能的,相对来说还是比较直观的。
MultiBinds注解
在介绍MultiBinds注解之前,我们要先了解这样的一种需求,假如我们需要在Module里提供了很多相同类型的 对象,如果我们不使用Qualifer,就会导致同一类型重复绑定的错误。但是如果我们确实需要在一个Module里包含这些对象的创建,又不想创建N多的Qualifer,我们就可以使用MultiBind机制来达到我们的目的。
MultiBind机制允许我们为这些对象创建一个集合,这个集合必须是Set或者Map,这样在Component中,我们就可以暴露这个集合,通过集合来获取不同的对象。这个集合的创建有三种方法,我们用简短的代码例子说明一下。
- 使用@IntoSet或者@IntoMap
@Module(subcomponents = CarComponent.class)
public class EngineModule {
@Provides
@IntoSet
Engine provideEngineIntoSet2(){
return new Engine(2);
}
@Provides
@IntoMap
@StringKey("3")
Engine provideEngineIntoMap3(){
return new Engine(3);
}
}
这种方声明使用集合,我们的提供方法返回的还是原来的类型,但是Dagger2会根据注解,将它转换为Set或者Map类型,并调用这些方法创建对象,将这些对象(与索引绑定之后,如果是Map类型的话)放入集合中。
- 直接提供Set或者Map类型
@Module(subcomponents = CarComponent.class)
public class EngineModule {
@Provides
Set<Engine> provideEngineSet(){
Set<Engine> engines = new HashSet<>();
engines .add(new Engine(9));
return engines;
}
@Provides
Map<String, Engine> provideEngineMap(){
Map<String,Engine> engineMap = new HashMap<>();
engineMap.put("engine10", new Engine(10));
return engineMap;
}
}
直接提供Set和Map就比较直观了,但是这种方式和使用前面一种是会冲突的,Dagger2允许使用@ElementIntoSet注解,将自定义的set元素添加到自动生成的Set中,但是Map是不能混用的,放在不同的Module中也不行。
- 使用@MultiBinds注解
@Module(subcomponents = CarComponent.class)
public abstract class AbsEngineModule {
@Multibinds
abstract Set<Engine> engineSet();
@Multibinds
abstract Map<String, Engine> engineMap();
}
MultiBinds只能用于标注抽象方法,它仅仅是告诉Component我有这么一种提供类型,让我们Component可以在Component中暴露Set或者Map类型的接口,但是不能包含具体的元素。Multibinds注解是可以和第一种集合定义混用的。
我在学习MultiBinds注解的时候,非常的郁闷。既然MultiBinds用于提供集合,为什么只能是空集?我要往里面添加元素,还得用@IntoSet和@IntoMap标注提供方法,而它们是可以独立工作的,MultiBinds的在这儿的作用不是等于0么?
是的,如果我们已知有哪些元素可以使用,完全没有必要用MultiBinds标签的,这个标签的作用,就是在不知道有哪些元素可以使用,但是我在Component里要暴露集合集合,以便其他地方可以调用。这在dagger-android框架里体现的就非常明显。它在框架中有这么一个类
@Module
public abstract class AndroidInjectionModule {
@Multibinds
abstract Map<Class<? extends Activity>, AndroidInjector.Factory<? extends Activity>>
activityInjectorFactories();
@Multibinds
abstract Map<Class<? extends Fragment>, AndroidInjector.Factory<? extends Fragment>>
fragmentInjectorFactories();
@Multibinds
abstract Map<Class<? extends Service>, AndroidInjector.Factory<? extends Service>>
serviceInjectorFactories();
@Multibinds
abstract Map<
Class<? extends BroadcastReceiver>, AndroidInjector.Factory<? extends BroadcastReceiver>>
broadcastReceiverInjectorFactories();
@Multibinds
abstract Map<
Class<? extends ContentProvider>, AndroidInjector.Factory<? extends ContentProvider>>
contentProviderInjectorFactories();
private AndroidInjectionModule() {}
}
这个Module里使用@Multibinds定义了四大组件加Fragment的AndroidInjector.Factory的集合,就是为了创建 DispatchingAndroidInjector<T>这个对象,我们看下它的构造方法:
DispatchingAndroidInjector(
Map<Class<? extends T>, Provider<AndroidInjector.Factory<? extends T>>> injectorFactories) {
this.injectorFactories = injectorFactories;
}
然后这个对象进行代理注入:
public boolean maybeInject(T instance) {
Provider<AndroidInjector.Factory<? extends T>> factoryProvider =
injectorFactories.get(instance.getClass());
if (factoryProvider == null) {
return false;
}
//此处省略N行代码
}
可以看到,它就是使用T的Class作为索引,从集合中取出对应的AndroidInject.Fractory对象进行工作的。而这其中的T,就是我们在App自己定义的各种组件,在dagger-android框架里,是不知道有哪些对象可以使用的。
总得来说,Multibinds注解的作用,就是为了Component提供依赖类型的完整性,这在编写一些框架给外部使用的时候,会起到关键的作用,其他时候,是可以使用@IntoXX标签替代的
现在我们还是继续看一下使用集合之后,Component里的写法:
@Component(modules = {EngineModule.class, AbsEngineModule.class})
public interface EngineComponent {
String getTag();
Set<Engine> engineSet();
Map<String, Engine> engineMap();
CarComponent.Builder carComponent();
@Component.Builder
interface Builder{
@BindsInstance
Builder tag(String tag);
Builder engineModule(EngineModule engineModule);
EngineComponent build();
}
}
我们在看看main中的测试写法:
private static void testEngine(EngineComponent engineComponent) {
System.out.println("EngineComponent tag : " + engineComponent.getTag());
for (Engine engine : engineComponent.engineSet()) {
System.out.print("engine in set : " + engine + "; cylinders : " + engine.getCylinderNumbers() + "\n");
}
for (Engine engine : engineComponent.engineMap().values()) {
System.out.print("engine in map : " + engine + "; cylinders : " + engine.getCylinderNumbers() + "\n");
}
}
打印的结果是:
EngineComponent tag : tagged success
engine in set : com.example.dagger2.Engine@3f99bd52; cylinders : 2
engine in map : com.example.dagger2.Engine@548c4f57; cylinders : 3
说明结果是正确的。
我们再看看关键的实现源码:
@Override
public Set<Engine> engineSet() {
return ImmutableSet.<Engine>of(
EngineModule_ProvideEngineIntoSet2Factory.proxyProvideEngineIntoSet2(engineModule));
}
@Override
public Map<String, Engine> engineMap() {
return ImmutableMap.<String, Engine>of(
"5", EngineModule_ProvideEngineIntoMap3Factory.proxyProvideEngineIntoMap3(engineModule));
}
我们在调用engineSet或者engineMap方法,获取对应集合的时候,Dagger直接通过Module实例创建我们要的对象,然后放到ImmutableXX里(这是个对象固定的集合),也就是说通过第一和第三种方式暴露的集合,对象引用是不可以替换的。使用第二种的,和我们正常暴露的普通类型是一样的,大家可以自己试验一下。
小结
本篇文章介绍了三种间接定义类型的方法,让我们可以更加灵活地使用类型绑定来构造对象。到此为止,Dagger的主要功能,我们已经介绍完毕了,下一篇,我们讲解dagger-android库,是时候真刀真枪地玩了。