学习Dagger2笔记:【8】注入到Set和Map容器

目录

0. 前言
1. 依赖与注入
2. @Inject
3. @Module & @Provides
4. @Component
5. @Qualifier
6. Provider & Lazy
7. @Scope
8. 注入到Set和Map容器
9. Bind系列注解
10. dagger中依赖关系与继承关系
11. dagger.android

目标

我们知道,每台电脑中都有硬盘,并且可能不止一张,每个电脑也会连接很多外设,本文的目的就在于将这些东西用dagger注入到我们的Computer

@IntoSet & @ElementsIntoSet

每张硬盘有着不同的类型(机械硬盘和固态硬盘),还有者不同的容量,在Computer中如果将每张硬盘都单独作为一个成员变量,未免扩展性太低了,因此我们用Set容器保存这些硬盘:

class Disk(private val type: Type, private val capacity: Capacity) { // 新建一个硬盘类
    fun mount(builder: StringBuilder) { // 此方法代表硬盘挂载到电脑上
        builder.append(capacity.size).append("G ").append(type.name).append(" mounted").append("\n")
    }

    enum class Type { HARD, SSD } // 硬盘的两种类型:机械和固体

    enum class Capacity(val size: Int) { SMALL(256), NORMAL(512), HUGE(1024) } // 硬盘的几种容量规格
}

abstract class Computer(private val os: String, private val price: Int) {
    /* ... */
    @field:Inject lateinit var disks: Set<Disk> // Computer中所有的硬盘
    /* ... */
    fun execute(builder: StringBuilder) { // CPU执行时除了返回自身信息,还要将CPU和Memory的信息一并返回
        /* ... */
        disks.forEach { it.mount(builder) } // 挂载所有硬盘
    }
}

这里我们自然可以让@Provides注解的方法返回一个Set容器类,但在dagger中还有其他方式也可以做到:

@Module
class DiskModule { // 新建一个硬盘数据仓库,用以给Computer提供硬盘
    @[Provides IntoSet] fun provideSmallHardDisk() = Disk(Disk.Type.HARD, Disk.Capacity.SMALL)
    @[Provides IntoSet] fun provideHugeHardDisk() = Disk(Disk.Type.HARD, Disk.Capacity.HUGE)

    @[Provides ElementsIntoSet]
    fun provideSSD() = setOf(
        Disk(Disk.Type.SSD, Disk.Capacity.SMALL),
        Disk(Disk.Type.SSD, Disk.Capacity.NORMAL)
    )
}

@Component(modules = [MemoryModule::class, DiskModule::class]) // 在Computer使用的Component桥接类中添加上硬盘数据仓库
interface ComputerComponent {
    fun inject(target: Computer)
}

没错,就像上面那样使用@IntoSet@ElementsIntoSet注解,就可以实现Set容器的注入了

照旧我们来看看生成的源码,打开相关Factory工厂类我们发现和普通使用@Provides生成的工厂类并无区别,看来又是@Component生成的桥接类有变动了:

public final class DaggerComputerComponent implements ComputerComponent {
  /* ... */
  private final DiskModule diskModule;
  /* ... */
  private Set<Disk> getSetOfDisk() {
    return SetBuilder.<Disk>newSetBuilder(3)
        .add(DiskModule_ProvideSmallHardDiskFactory.provideSmallHardDisk(diskModule))
        .add(DiskModule_ProvideHugeHardDiskFactory.provideHugeHardDisk(diskModule))
        .addAll(DiskModule_ProvideSSDFactory.provideSSD(diskModule))
        .build();
  }
  /* ... */
  private Computer injectComputer(Computer instance) {
	/* ... */
    Computer_MembersInjector.injectSetDisks(instance, getSetOfDisk());
    return instance;
  }
  /* ... */
}

多了一个getSetOfDisk()方法,这个方法将各个@Provides提供的数据整合在一起,通过SetBuilder的Builder模式构建新的Set容器,对于@IntoSet标记的单个元素调用其add()方法,而对于@ElementsIntoSet标记的容器则调用addAll()方法:

public final class SetBuilder<T> {
  private static final String SET_CONTRIBUTIONS_CANNOT_BE_NULL =
      "Set contributions cannot be null";
  private final List<T> contributions;

  private SetBuilder(int estimatedSize) {
    contributions = new ArrayList<>(estimatedSize);
  }
    
  public static <T> SetBuilder<T> newSetBuilder(int estimatedSize) {
    return new SetBuilder<T>(estimatedSize);
  }

  public SetBuilder<T> add(T t) {
    contributions.add(checkNotNull(t, SET_CONTRIBUTIONS_CANNOT_BE_NULL));
    return this;
  }

  public SetBuilder<T> addAll(Collection<? extends T> collection) {
    for (T item : collection) {
      checkNotNull(item, SET_CONTRIBUTIONS_CANNOT_BE_NULL);
    }
    contributions.addAll(collection);
    return this;
  }

  public Set<T> build() {
    switch (contributions.size()) {
      case 0:
        return Collections.emptySet();
      case 1:
        return Collections.singleton(contributions.get(0));
      default:
        return Collections.unmodifiableSet(new HashSet<>(contributions));
    }
  }
}

SetBuilder类整体上看起来就非常简单了,先用ArrayList将所有元素存起来,build()的时候再生成新的Set容器,这里需要注意生成的Set容器是不能修改的,即调用add()set()remove()等都是会抛异常的

另外Set容器的注入还可以搭配@Qualifier(注意是对Set容器的别名,而不是其中元素的别名)、ProviderLazy等(例如Lazy>),大家自行尝试吧

@IntoMap & 各种“Key”

我们知道Map相较于Set,除了将目标对象存入容器,还得为其指定一个Key值以区分它们。现在有鼠标、键盘、音响等外设需要连接到Computer上:

interface Device { // 外设接口
    fun connect(builder: StringBuilder) // 每种外设都需要连接到Computer
}

class Mouse : Device { // 鼠标
    override fun connect(builder: StringBuilder) {
        builder.append(" move").append("\n")
    }
}

class Keyboard : Device { // 键盘
    override fun connect(builder: StringBuilder) {
        builder.append(" press").append("\n")
    }
}

class Sound : Device { // 音响
    override fun connect(builder: StringBuilder) {
        builder.append(" play").append("\n")
    }
}

abstract class Computer(private val os: String, private val price: Int) {
    /* ... */
    @set:Inject
    lateinit var devices: Map<String, @JvmSuppressWildcards Device> // 使用Map存放连上电脑的外设,另外@JvmSuppressWildcards下面有说明
    /* ... */
    fun execute(builder: StringBuilder) { // CPU执行时除了返回自身信息,还要将CPU和Memory的信息一并返回
        /* ... */
        devices.forEach { (name, device) -> // 连接各外设
            builder.append(name).append(": ")
            device.connect(builder)
        }
    }
}

特别注意因为Device是一个抽象类,并且kotlin中对map的定义为Map,因此在编译时dagger将其看做Map,但根本不会有? extends V这种类型,所以需要添加@JvmJvmSuppressWildcards这个注解,让其在编译时无视掉这种泛型变换。具体可以看下stackoverflow上的文章

另外还要注意这里的Map容器是以String作为Key值的,因此需要用到dagger提供的@StringKey@IntoMap注解:

@Module
class DeviceModule { // 外设数据仓库,注意除了需要使用@IntoMap,还需要用@StringKey指定数据在map容器中对应的key值
    @[Provides IntoMap StringKey("Mouse")]
    fun provideMouse(): Device = Mouse()

    @[Provides IntoMap StringKey("Keyboard")]
    fun provideKeyboard(): Device = Keyboard()

    @[Provides IntoMap StringKey("Sound")]
    fun provideSound(): Device = Sound()
}

@Component(modules = [MemoryModule::class, DiskModule::class, DeviceModule::class]) // 在Computer使用的Component桥接类中添加Device的数据仓库
interface ComputerComponent {
    fun inject(target: Computer)
}

我想你已经猜到生成的Component长啥样了吧:

public final class DaggerComputerComponent implements ComputerComponent {
  /* ... */
  private final DeviceModule deviceModule;
  /* ... */
  private Map<String, Device> getMapOfStringAndDevice() {
    return MapBuilder.<String, Device>newMapBuilder(3)
        .put("Mouse", DeviceModule_ProvideMouseFactory.provideMouse(deviceModule))
        .put("Keyboard", DeviceModule_ProvideKeyboardFactory.provideKeyboard(deviceModule))
        .put("Sound", DeviceModule_ProvideSoundFactory.provideSound(deviceModule))
        .build();
  }
  /* ... */
  private Computer injectComputer(Computer instance) {
    /* ... */
    Computer_MembersInjector.injectSetDevices(instance, getMapOfStringAndDevice());
    return instance;
  }
}

套路简直和@IntoSet一模一样,直接看MapBuilder

public final class MapBuilder<K, V> {
  private final Map<K, V> contributions;

  private MapBuilder(int size) {
    contributions = newLinkedHashMapWithExpectedSize(size);
  }
    
  public static <K, V> MapBuilder<K, V> newMapBuilder(int size) {
    return new MapBuilder<>(size);
  }

  public MapBuilder<K, V> put(K key, V value) {
    contributions.put(key, value);
    return this;
  }

  public MapBuilder<K, V> putAll(Map<K, V> map) {
    contributions.putAll(map);
    return this;
  }

  public Map<K, V> build() {
    switch (contributions.size()) {
      case 0:
        return Collections.emptyMap();
      default:
        return Collections.unmodifiableMap(contributions);
    }
  }
}

MapBuilder整体上也是非常简单的,内部先用LinkedHashMap保存数据,build()的时候生成新的map容器,同样也要注意生成的map容器是不能修改的

这里除了通过@StringKey指定map的Key为String类型以外,还有@ClassKey@IntKey@LongKey这些注解辅助map的注入

有待完善

有时候我们Map的Key值并非是StringClassIntLong中的其中一种,可能是几种类型的复合、含有数据甚至是一个自定义的类,dagger给出了@MapKey+@AutoAnnotation的方案来解决复合类型的Key值,但需要额外引入google的AutoValue框架,而且并不能解决自定义类作为Key值的注入。这里就不再过多尝试,如果真的有这种需求场景,我们也可以通过@Provides直接返回对应Map(这种方法还适用于编译时期不能确定Key值,需要运行时动态注入的情况)

总结

当我们需要将依赖注入到SetMap容器时,可以考虑使用@IntoSet@ElementsIntoSet@IntoMap这些注解,但一些复杂情况还是建议通过@Provides返回SetMap容器

你可能感兴趣的:(Android)