翻译Dagger 2多元绑定
官方文档链接:https://google.github.io/dagger/multibindings.html
1.前言
Dagger通过多元绑定可以将几个对象放入一个集合中,即使它们是由不同的Module提供。集合的组装由Dagger自己完成,所以往代码中注入时,不需要直接依赖于每个对象。使用多元绑定可以实现一个插件架构,举个例子,由几个Moule分别提供插件接口的实现,核心类就可以使用一整套的插件。或者将几个Module分别提供的Service,以名字为键放入Map。
2.Set的使用
为了给可注入的Set提供元素,需为Module的方法添加@IntoSet
注解:
@Module
class MyModuleA {
@Provides @IntoSet
static String provideOneString(DepA depA, DepB depB) {
return "ABC";
}
}
若想一次性提供多个元素,可增加返回值是一个Set的Module方法,并且注解为@ElementsIntoSet
:
@Module
class MyModuleB {
@Provides @ElementsIntoSet
static Set<String> provideSomeStrings(DepA depA, DepB depB) {
return new HashSet<String>(Arrays.asList("DEF", "GHI"));
}
}
在Component的依赖图中,可直接依赖Set:
class Bar {
@Inject Bar(Set<String> strings) {
assert strings.contains("ABC");
assert strings.contains("DEF");
assert strings.contains("GHI");
}
}
或者通过Component向外提供Set:
@Component(modules = {MyModuleA.class, MyModuleB.class})
interface MyComponent {
Set<String> strings();
}
@Test void testMyComponent() {
MyComponent myComponent = DaggerMyComponent.create();
assertThat(myComponent.strings()).containsExactly("ABC", "DEF", "GHI");
}
与其它依赖关系一样,除了提供Set<Foo>
,还有Provider<Set<Foo>>
或Lazy<Set<Foo>>
。但是,不能依赖Set<Provider<Foo>>
。为了提供修饰过的Set,给@Provides
注解的方法添加修饰注解:
@Module
class MyModuleC {
@Provides @IntoSet
@MyQualifier
static Foo provideOneFoo(DepA depA, DepB depB) {
return new Foo(depA, depB);
}
}
@Module
class MyModuleD {
@Provides
static FooSetUser provideFooSetUser(@MyQualifier Set<Foo> foos) { ... }
}
3.Map的使用
Dagger通过多元绑定给可注入的Map提供实例,只要Map的键在编译时被知道。与Set有点不同,除了添加能返回值的Module方法,并用@IntoMap
注解,还需要另一个自定义的注解来给提供的实例指定Map的键。若是给修饰过的Map提供实例,须给@IntoMap
注解的方法添加修饰注解。
可以注入Map本身(Map<K, V>
)或者值y用被Provider包裹的Map(Map<K, Provider<V>>
)。后者可以实现集合中的值不全部初始化,比如希望每次获取一个值,或者希望每次查询Map时,即使同一个值也是新的对象。
3.1.简单的键
当Map的键是字符串、Class<?>
或者装箱的基本类型时,使用dagger.multibindings
包中的标准注解:
@Module
class MyModule {
@Provides @IntoMap
@StringKey("foo")
static Long provideFooValue() {
return 100L;
}
@Provides @IntoMap
@ClassKey(Thing.class)
static String provideThingValue() {
return "value for Thing";
}
}
@Component(modules = MyModule.class)
interface MyComponent {
Map<String, Long> longsByString();
Map<Class<?>, String> stringsByClass();
}
@Test void testMyComponent() {
MyComponent myComponent = DaggerMyComponent.create();
assertThat(myComponent.longsByString().get("foo")).isEqualTo(100L);
assertThat(myComponent.stringsByClass().get(Thing.class))
.isEqualTo("value for Thing");
}
若键是枚举类型或具体的参数化类,则声明一个@MapKey
注解的注解类型,它的唯一成员类型是键的类型:
enum MyEnum {
ABC, DEF;
}
@MapKey
@interface MyEnumKey {
MyEnum value();
}
@MapKey
@interface MyNumberClassKey {
Class<? extends Number> value();
}
@Module
class MyModule {
@Provides @IntoMap
@MyEnumKey(MyEnum.ABC)
static String provideABCValue() {
return "value for ABC";
}
@Provides @IntoMap
@MyNumberClassKey(BigDecimal.class)
static String provideBigDecimalValue() {
return "value for BigDecimal";
}
}
@Component(modules = MyModule.class)
interface MyComponent {
Map<MyEnum, String> myEnumStringMap();
Map<Class<? extends Number>, String> stringsByNumberClass();
}
@Test void testMyComponent() {
MyComponent myComponent = DaggerMyComponent.create();
assertThat(myComponent.myEnumStringMap().get(MyEnum.ABC)).isEqualTo("value for ABC");
assertThat(myComponent.stringsByNumberClass.get(BigDecimal.class))
.isEqualTo("value for BigDecimal");
}
自定义的注解可以是任何名字,它的唯一成员可以是除数组外的任何有效的注解成员类型。
3.2.复杂的键
如果Map的键需要不只单个注解成员才能表达,可通过给自定义注解的@MapKey
注解设置unwrapValue = false
,来使用整个注解(包括多个注解成员)作为键。此时,自定义的注解可以拥有数组成员。
@MapKey(unwrapValue = false)
@interface MyKey {
String name();
Class<?> implementingClass();
int[] thresholds();
}
@Module
class MyModule {
@Provides @IntoMap
@MyKey(name = "abc", implementingClass = Abc.class, thresholds = {1, 5, 10})
static String provideAbc1510Value() {
return "foo";
}
}
@Component(modules = MyModule.class)
interface MyComponent {
Map<MyKey, String> myKeyStringMap();
}
使用@AutoAnnotation
创建注解实例。如果使用复杂的键,需要在运行时给自定义的注解创建实例,并传入Map的get(Object)
方法中。最简单的办法是,添加静态方法返回自定义注解的实例,并使用@AutoAnnotation
注解此方法。
class MyComponentTest {
@Test void testMyComponent() {
MyComponent myComponent = DaggerMyComponent.create();
assertThat(myComponent.myKeyStringMap()
.get(createMyKey("abc", Abc.class, new int[] {1, 5, 10}))
.isEqualTo("foo");
}
@AutoAnnotation
static MyKey createMyKey(String name, Class<?> implementingClass, int[] thresholds) {
return new AutoAnnotation_MyComponentTest_createMyKey(name, implementingClass, thresholds);
}
}
3.3.编译时不知道键
Map只在编译时已知键且用注解表示出来的情况下,才会工作。如果不满足这些限制,就不能直接使用,但是可以通过Set存储一系列的对象,然后转换成普通的Map。
@Module
class MyModule {
@Provides @IntoSet
static Map.Entry<Foo, Bar> entryOne(...) {
Foo key = ...;
Bar value = ...;
return new SimpleImmutableEntry(key, value);
}
@Provides @IntoSet
static Map.Entry<Foo, Bar> entryTwo(...) {
Foo key = ...;
Bar value = ...;
return new SimpleImmutableEntry(key, value);
}
}
@Module
class MyMapModule {
@Provides
static Map<Foo, Bar> fooBarMap(Set<Map.Entry<Foo, Bar>> entries) {
Map<Foo, Bar> fooBarMap = new LinkedHashMap<>(entries.size());
for (Map.Entry<Foo, Bar> entry : entries) {
fooBarMap.put(entry.getKey(), entry.getValue());
}
return fooBarMap;
}
}
通过这种方法,若想普通Map中含有Provider包裹的值,构建Set的Map.Entry
对象也应该包含Provider。
@Module
class MyModule {
@Provides @IntoSet
static Map.Entry<Foo, Provider<Bar>> entry(
Provider<BarSubclass> barSubclassProvider) {
Foo key = ...;
return new SimpleImmutableEntry(key, barSubclassProvider);
}
}
@Module
class MyProviderMapModule {
@Provides
static Map<Foo, Provider<Bar>> fooBarProviderMap(
Set<Map.Entry<Foo, Provider<Bar>>> entries) {
return ...;
}
}
4.声明多元绑定
通过给Module增加抽象的、被@Multibinds
注解的方法,并返回希望声明的Set或Map。若Set或Map已经被@IntoSet
、@ElementsIntoSet
、@IntoMap
中至少一个注解,那么不应该使用@Multibinds
注解,除非它们是空集合。
@Module
abstract class MyModule {
@Multibinds abstract Set<Foo> aSet();
@Multibinds @MyQualifier abstract Set<Foo> aQualifiedSet();
@Multibinds abstract Map<String, Foo> aMap();
@Multibinds @MyQualifier abstract Map<String, Foo> aQualifiedMap();
}
提供的Set或Map可以被声明很多次都不会出错。Dagger不会实现或调用任何@Multibinds
注解的方法。
5.返回空Set
作为一种针对空Set的备选的解决方案,添加@ElementsIntoSet
注解的返回空Set的方法。
@Module
class MyEmptySetModule {
@Provides @ElementsIntoSet
static Set<Foo> primeEmptyFooSet() {
return Collections.emptySet();
}
}
6.继承Subcomponent的多元绑定
Subcomponent可以依赖从它父Component那继承到的Set和Map,就像可以依赖它父Component的其它关系一样。但是,Subcomponent可以通过给Module添加适当的@Provides
注解的方法,来给继承到的Set和Map添加元素。此时,Set和Map不同于注入时,除了含有Subcomponent定义的值或Map.Entry
,同时还包含父Component定义的。
@Component(modules = ParentModule.class)
interface ParentComponent {
Set<String> strings();
Map<String, String> stringMap();
ChildComponent childComponent();
}
@Module
class ParentModule {
@Provides @IntoSet
static String string1() {
"parent string 1";
}
@Provides @IntoSet
static String string2() {
"parent string 2";
}
@Provides @IntoMap
@StringKey("a")
static String stringA() {
"parent string A";
}
@Provides @IntoMap
@StringKey("b")
static String stringB() {
"parent string B";
}
}
@Subcomponent(modules = ChildModule.class)
interface ChildComponent {
Set<String> strings();
Map<String, String> stringMap();
}
@Module
class ChildModule {
@Provides @IntoSet
static String string3() {
"child string 3";
}
@Provides @IntoSet
static String string4() {
"child string 4";
}
@Provides @IntoMap
@StringKey("c")
static String stringC() {
"child string C";
}
@Provides @IntoMap
@StringKey("d")
static String stringD() {
"child string D";
}
}
@Test void testMultibindings() {
ParentComponent parentComponent = DaggerParentComponent.create();
assertThat(parentComponent.strings()).containsExactly(
"parent string 1", "parent string 2");
assertThat(parentComponent.stringMap().keySet()).containsExactly("a", "b");
ChildComponent childComponent = parentComponent.childComponent();
assertThat(childComponent.strings()).containsExactly(
"parent string 1", "parent string 2", "child string 3", "child string 4");
assertThat(childComponent.stringMap().keySet()).containsExactly(
"a", "b", "c", "d");
}
7.总结
使用多元绑定时,会让集合出现的莫名其妙,对其中的元素也很难掌握清楚。建议根据个人的情况进行使用,但跨Module供值的确是优势。