[Dagger Official Reference] Multibindings

08 Dec 2016

Dagger는 multibindings을 사용하여 여러 객체들을 심지어 다른 모듈에 바인딩된 경우에도 컬렉션 안에 바인딩할 수 있다. Dagger는 컬렉션을 모아서 애플리케이션 코드가 개별 바인딩에 직접 의존하지 않고도 주입될 수 있도록 한다.

예를 들어, Multibindings을 사용하여 여러 모듈이 개별 플러그인 인터페이스 구현을 제공하여 중앙의 클래스가 전체 플러그인 세트를 사용할 수 있는 플러그인 아키텍처를 구현할 수 있다. 또는 여러 모듈들을 개별 서비스 제공자provider에게 Key가 name인 Map으로 제공할 수도 있다.

Multibindings 설정

한 요소를 주입가능한 multibound 세트에 제공하기 위해서는 당신의 모듈에 @IntoSet 어노테이션을 추가하라.

@Module
class MyModuleA {
  @Provides @IntoSet
  static String provideOneString(DepA depA, DepB depB) {
    return "ABC";
  }
}

또한 당신은 @ElementsIntoSet으로 어노테이트되고 subset을 반환하는 모듈 메소드를 추가하여 여러 항목들을 한 번에 제공할 수도 있다.

@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");

다른 바인딩과 마찬가지로, multibound Set에 의존할 뿐만 아니라, `Provider<Set>` 또는 `Lazy<Set>`에도 의존할 수 있다. 하지만 `<Set<Provider>`에는 의존할 수 없다.

Qualified multibound set을 제공하기 위해서는, 각 @Provides 메소드에 qualifier 어노테이션을 추가하라:

@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) { … }
}

Map multibindings

컴파일 타임에 map의 key를 알 수 있다면 multibindings를 사용하여 주입가능한 map에 entry를 제공할 수 있다.

Multibinding map에 entry를 제공하려면 @IntoMap 어노테이션과 entry을 위한 map의 key를 지정하는 커스텀 어노테이션을 가지고 그 value을 반환하는 메소드를 모듈에 추가하라. 각 Qualified multibound map에 entry을 제공하기 위해서는 @IntoMap 메소드에 Qualifier 어노테이션을 추가하라.

그 다음에는 map 자체(Map<K,V>) 또는 value provider를 포함한 map(Map<K, Provider<V>)를 주입할 수 있다. 후자는 한 번에 하나의 value만을 추출하고자 하거나, 어쩌면 Map을 쿼리할 때 마다 각 value의 새로운 인스턴스를 가져오고자 할 때, 모든 value들이 인스턴스화 되는 것을 원하지 않으려는 경우에 유용한다.

간단한 Map keys

스트링, Class<?> 또는 박스형 primitive을 가진 Map의 경우, dagger.mapKeys의 표준 어노테이션 중 하나를 사용하라:

@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");
}

Enum 또는 구체적으로 매개변수화된parameterized 클래스가 key인 map의 경우 타입이 Map의 key 타입인 멤버를 가진 어노테이션을 작성하고, @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");
}

당신의 어노테이션의 단일 멤버는 배열을 제외하면 모두 유요한 어노테이션 멤버가 될 수 있으며, 임의의 이름을 가질 수 있다.

복잡한 Map keys

Map의 key가 단일 어노테이션 멤버만으로 표현될 수 없다면, @MapKeyunwrapValuefalse로 설정함으로서 전체 어노테이션을 map의 key로 사용할 수 있다. 이 경우, 어노테이션은 배열 구성원들도 가질 수 있다.

@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이 복잡한 key를 사용하는 경우 런타임에 @MapKey 어노테이션의 인스턴스를 만들어 map의 get(Object) 메소드에 전달할 필요가 있을 수 있다. 이를 위한 가장 간단한 방법은 @AutoAnnotation 어노테이션을 사용하여 당신의 어노테이션을 인스턴스화하는 static 메소드를 만드는 것이다. 자세한 내용은 @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);
  }
}

컴파일타임에 key를 알지 못하는 Map

multibinding은 Map의 key가 컴파일 타임에 알 수 있고 어노테이션으로 표현 될 수 있는 경우에만 동작한다. 만약 Map의 key가 이런 제약 조건에 맞지 않는 경우, multibound Map을 만들 수 없다. 하지만 multibound Map이 아니도록 변환할 수 있는 객체의 Set을 바인딩하기 위해 Set multibinding을 사용하면 이 문제를 해결할 수 있다.

@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<Foo, Provider<Bar>> 같은 자동화된 바인딩을 제공해주지 않는다. 만약 Provider의 map을 원한다면, multibound set안의 Map.Entry 객체가 provider를 포함해야 한다. 그러면 multibound map은 Provider value를 가질 수 있다.

@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 …;
  }
}

Multibindings 선언하기

선언할 set이나 map을 반환하는 @Multibindings-어노테이션된 추상 메소드를 모듈에 추가하여 바인됭된 Multibindings set이나 map을 선언할 수 있다.

최소한 하나의 @IntoSet, @ElementsIntoSet 또는 @IntoMap 바인딩을 가진 set이나 map에 대해 @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 multibinding은 오류없이 여러 번 선언 될 수 있다. Dagger는 절대로 @Multibinds 메소드를 구현하거나 호출하지 않는다.

대안 : 빈 set를 반환하는 @ElementsIntoSet

빈 set의 경우에만, 대안으로, 빈 set을 반환하는 @ElementsIntoSet 메소드를 추가할 수 있다.

@Module
class MyEmptySetModule {
  @Provides @ElementsIntoSet
  static Set<Foo> primeEmptyFooSet() {
    return Collections.emptySet();
  }
}

상속된 subcomponent multibindings

Subcomponent의 바인딩은 자신의 부모의 다른 바인딩에 의존할 수 있는 것처럼 부모의 multibound set이나 map에도 의존할 수 있다. 그러나 subcomponent는 자신의 module안에 적절한 @Provides 메소드를 포함함으로서 부모에 바인딩된 multibound set이나 map에 항목을 추가할 수 있다.

그런 일이 발생하면, set이나 map은 주입 위치에 따라 달라진다. subcomponent에 정의된 바인딩에 주입되면, 부모 component의 multibinding에 정의된 것과 subcomponent의 multibinding에 정의된 것의 value나 entry를 가진다. 부모 component에 정의된 바인딩에 주입되면 그곳에 정의된 value나 entry만을 가지게 된다.

@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");
}