Dagger allows you to bind several objects into a collection even when the objects are bound in different modules using multibindings. Dagger assembles the collection so that application code can inject it without depending directly on the individual bindings.
You could use multibindings to implement a plugin architecture, for example, where several modules can contribute individual plugin interface implementations so that a central class can use the entire set of plugins. Or you could have several modules contribute individual service providers to a map, keyed by name.
Set multibindings
In order to contribute one element to an injectable multibound set, add an
@IntoSet
annotation to your module method:
@Module
class MyModuleA {
@Provides @IntoSet
static String provideOneString(DepA depA, DepB depB) {
return "ABC";
}
}
You can also contribute several elements at one time by adding a module method
that returns a subset and is annotated with @ElementsIntoSet
:
@Module
class MyModuleB {
@Provides @ElementsIntoSet
static Set<String> provideSomeStrings(DepA depA, DepB depB) {
return new HashSet<String>(Arrays.asList("DEF", "GHI"));
}
}
Now a binding in that component can depend on the set:
class Bar {
@Inject Bar(Set<String> strings) {
assert strings.contains("ABC");
assert strings.contains("DEF");
assert strings.contains("GHI");
}
}
Warning:
With Kotlin sources, the type argument of a multibinding set may end up
being replaced with a wildcard type,
i.e. Foo
turns into ? extends Foo
. In such cases you might get an error
like the following:
[Dagger/MissingBinding] Set<? extends Foo> cannot be provided without an @Provides-annotated method.
To fix such an error you need to annotate your element
with @JvmSuppressWildcards
at the injection site, e.g.
@Inject lateinit var foo: Set<@JvmSuppressWildcards Foo>
.
Or the component can provide the 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");
}
As with any other binding, in addition to depending on a multibound Set<Foo>
,
you can also depend on Provider<Set<Foo>>
or Lazy<Set<Foo>>
. You cannot,
however, depend on Set<Provider<Foo>>
.
To contribute to a qualified multibound set, annotate each @Provides
method
with the 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
Dagger lets you use multibindings to contribute entries to an injectable map as long as the map keys are known at compile time.
To contribute an entry to a multibound map, add a method to a module that
returns the value and is annotated with @IntoMap
and with
another custom annotation that specifies the map key for that entry. Keys must
be unique, and any collision within a Map will result in a compile-time error.
To contribute an entry to a qualified multibound map, annotate each @IntoMap
method with the qualifier.
Then you can inject either the map itself (Map<K, V>
) or a map containing
value providers (Map<K, Provider<V>>
). The latter is useful for various
reasons. For example, it can be useful when you don’t want all of the values to
be instantiated, or because you want to handle Provider exceptions from
entries, or because you want to get a potentially new instance of each value
each time you query the map.
Warning:
With Kotlin sources, the type argument of a multibinding set may end up
being replaced with a wildcard type,
i.e. Foo
turns into ? extends Foo
. In such cases you might get an error
like the following:
[Dagger/MissingBinding] Map<String, ? extends Foo> cannot be provided without an @Provides-annotated method.
To fix such an error you need to annotate your element
with @JvmSuppressWildcards
at the injection site, e.g.
@Inject lateinit var foo: Map<String, @JvmSuppressWildcards Foo>
.
Simple map keys
For maps with keys that are strings, Class<?>
, or boxed primitives, use one of
the standard annotations in 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");
}
For maps with keys that are enums or a more specifically parameterized class,
write an annotation type with one member whose type is the map key type, and
annotate it with @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");
}
Your annotation’s single member can be any valid annotation member type except for arrays, and can have any name.
Lazy class loading
Using @ClassKey
or a custom annotation with a Class
type for the annotation
member, allows you to inject a class keyed map. One issue with this approach is
that all the classes in the key set will be loaded when constructing the map.
Depending on the size of the map, this may cause performance problems in
environments like Android. To avoid the unnecessary class loading,
@LazyClassKey
can be used to generate a string keyed map under the hood,
so that classes are only loaded when used for a lookup.
The @LazyClassKey
map does not support returning keySet()
or entrySet()
as
they will load all class keys and degrade the benefit of using the annotation.
Usage of unqualified @LazyClassKey
cannot co-exist with @ClassKey
. This
feature works by using the class names, and so is paired with configuration for
R8 and Proguard. In Proguard, the names of classes used as keys will be kept as
a result of this feature.
Complex map keys
If your map’s key is more than can be expressed by a single annotation member,
you can use the entire annotation as the map key by setting @MapKey
’s
unwrapValue
to false
. In that case, the annotation can have array members as
well.
@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();
}
Using @AutoAnnotation
to create annotation instances
If your map uses complex keys, then you may need to create an instance of your
@MapKey
annotation at run-time to pass to the map’s get(Object)
method. The
easiest way to do that is to use the @AutoAnnotation
annotation to create a
static method that instantiates your annotation. See
@AutoAnnotation
’s documentation for more details.
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);
}
}
Maps whose keys are not known at compile time
Map multibindings work only if your map’s keys are known at compile time and can be expressed in an annotation. If your map’s keys don’t fit in those constraints, then you cannot create a multibound map, but you can work around that by using set multibindings to bind a set of objects that you can then transform into a non-multibound 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;
}
}
Note that this technique does not give you the automatic binding of
Map<Foo, Provider<Bar>>
as well. If you want a map of providers, the
Map.Entry
objects in your multibound set should include the providers. Then
your non-multibound map can have Provider
values.
@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 ...;
}
}
Declaring multibindings
You can declare that a multibound set or map is bound by adding an abstract
@Multibinds
-annotated method to a module that returns the set or map you
want to declare.
You do not have to use @Multibinds
for sets or maps that have at least one
@IntoSet
, @ElementsIntoSet
, or @IntoMap
binding, but you do have to
declare them if they may be empty.
@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();
}
A given set or map multibinding can be declared any number of times without
error. Dagger never implements or calls any @Multibinds
methods.
Alternative: @ElementsIntoSet
returning an empty set
For empty sets only, as an alternative, you can add a @ElementsIntoSet
method that returns an empty set:
@Module
class MyEmptySetModule {
@Provides @ElementsIntoSet
static Set<Foo> primeEmptyFooSet() {
return Collections.emptySet();
}
}
Inherited subcomponent multibindings
A binding in a subcomponent can depend on a multibound set or map from its
parent, just as it can depend on any other binding from its parent. But a
subcomponent can add elements to multibound sets or maps that are bound in its
parent as well, by simply including the appropriate @Provides
methods in its
modules.
When that happens, the set or map is different depending on where it is injected. When it is injected into a binding defined on the subcomponent, then it has the values or entries defined by the subcomponent’s multibindings as well as those defined by the parent component’s multibindings. When it is injected into a binding defined on the parent component, it has only the values or entries defined there.
@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");
}