美文网首页
Dagger2 依赖的接力游戏(六):Bind、BindInst

Dagger2 依赖的接力游戏(六):Bind、BindInst

作者: 散落_a0b3 | 来源:发表于2019-01-04 23:41 被阅读0次

文接《Dagger2 依赖的接力游戏(五)》

本篇示例代码收录在项目的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中,我们就可以暴露这个集合,通过集合来获取不同的对象。这个集合的创建有三种方法,我们用简短的代码例子说明一下。

  1. 使用@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类型的话)放入集合中。

  1. 直接提供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中也不行。

  1. 使用@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库,是时候真刀真枪地玩了。

相关文章

网友评论

      本文标题:Dagger2 依赖的接力游戏(六):Bind、BindInst

      本文链接:https://www.haomeiwen.com/subject/gbzkrqtx.html