Dagger 2 Multibindings Illustrated

Elye
Elye
Oct 20 · 7 min read
Picture by Masaaki Komori on Unsplash

Dagger 2 has a multibindings capability with official reference as below. It took me quite a while to understand and get working example up.

So decided to make easier illustration on it, and make all the examples in github (note: the code is in Java, to easier match the original Reference document. If you like to see the Kotlin code version, refer here).

Dagger 2 Multibindings is simply a feature that package dependencies from different modules into either a SET or MAP of dependencies. It could then be injected all together to the target object.

Set multibindings

We use @IntoSet to bind a value into the Set Collection

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

Similarly we use @ElementsIntoSet to bind a set into the Set Collection

@Module
class MyModuleB {
@Provides
@ElementsIntoSet
static Set<String> provideSomeStrings(DepA depA, DepB depB) {
return new HashSet<String>(Arrays.asList("DEF", "GHI"));
}
}

To confirm, we have a Component as below

@Component(modules = {MyModuleA.class, MyModuleB.class})
interface MyComponent {
Set<String> strings();
}

And we could verify that they have been multibinds into the Set<String> using the below test

@Test
public void testMyComponent() {
Set expectedContains =
new HashSet<String>(Arrays.asList("ABC", "DEF", "GHI"));

MyComponent myComponent = DaggerMyComponent.create();
assertEquals(3, myComponent.strings().size());
assertTrue(myComponent.strings().containsAll(expectedContains));
}

This is illustrated below.

Other than getting the Set<String>, you could also get the Provider<Set<String>> or Lazy<Set<String>>. Check out this tutorial if you don’t quite get what this means.

If we have two different set of strings, we could use Qualifier. Example code below where QualifySetOne and QualifySetTwo are used.

@Module
class MyModuleB {
@Provides
@ElementsIntoSet
@QualifySetOne
static Set<String> provideSomeStrings(DepA depA, DepB depB) {
return new HashSet<String>(Arrays.asList("DEF", "GHI"));
}
}
@Module
class MyModuleD {
@Provides
@ElementsIntoSet
@QualifySetTwo
static Set<String> provideSomeStrings(DepA depA, DepB depB) {
return new HashSet<String>(Arrays.asList("456", "789"));
}
}

To get more detail about qualifier, you could check out this tutorial.

Map multibindings

Other than set, you could also package your dependencies into a Map. The Key of the Map need to be known during compile time.

Simple Map

We can use @IntoMap for packaging into Map.

For maps with keys that are strings, Class<?>, or boxed primitives, use one of the standard annotations in dagger.multibindings. Two examples of Key is String (using @StringKey) and Class name (using @ClassKey).

@Module
class MyModule {
@Provides @IntoMap
@StringKey("foo")
static Long provideFooValue() {
return 100L;
}
@Provides @IntoMap
@StringKey("boo")
static Long provideBooValue() {
return 200L;
}
@Provides @IntoMap
@ClassKey(Thing.class)
static String provideThingValue() {
return "value for Thing";
}
}

We have our component as

@Component(modules = MyModule.class)
interface MyComponent {
Map<String, Long> longsByString();
Map<Class<?>, String> stringsByClass();
}

Then we could verify them

@Test
public void testMyComponent() {
MyComponent myComponent = DaggerMyComponent.create();

assertEquals(2, myComponent.longsByString().size());
assertEquals(100L,
myComponent.longsByString().get("foo").longValue());
assertEquals(200L,
myComponent.longsByString().get("boo").longValue());

assertEquals(1, myComponent.stringsByClass().size());
assertEquals("value for Thing",
myComponent.stringsByClass().get(Thing.class));
}

This could be further illustrated as below.

Other than getting the exact dependency, one could get the Provider or Lazy of factory of the dependency, just indicating the Provider (or Lazy) as it intended to obtained, as shown in the code below. Check out this tutorial if you don’t quite get what this means.

@Component(modules = MyModule.class)
interface MyComponent {
Map<String, Long> longsByString();
Map<Class<?>, String> stringsByClass();
Map<String, Provider<Long>> providerLongsByString();
}

Enum or SubClass Key

We could also make Enum Key, or Subclass Key using @MapKey as shown below.

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

Note: BigDecimal is a subclass of Number.

This could be easily illustrated below

@MapKey annotation is possible for any type except for Array.

Complex map keys

The simple map keys are for single-element key map. If you have more than one element as key (i.e. a combination of element to form a key), you’ll have to set @MapKey’s unwrapValue to false

Below is an example of

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

This could be illustrated below

To ensure the equivalent of Key, we’ll need to use AutoAnnotation, which is part of AutoValue's feature, that automatically makes JavaObject Content Equivalent comparison (instead of Object Equivalent)

public class AutoValueUtil {
@AutoAnnotation
static MyKey createMyKey(
String name, Class<?> implementingClass, int[] thresholds) {
return new AutoAnnotation_AutoValueUtil_createMyKey(
name, implementingClass, thresholds);
}
}

To read more about AutoAnnotation, refers to this Stackoverflow.

With the AutoAnnotation connected, you could now easily verify the injection of the key value from the map as below.

@Test
public void testMyComponent() {
MyComponent myComponent = DaggerMyComponent.create();
assertEquals("foo",
myComponent.myKeyStringMap().get(
createMyKey("abc", Abc.class, new int[] {1, 5, 10})));
}

Maps whose keys are not known at compile time

Dagger 2 Multibinds into Map can only work with case that has Key known during compile time.

However for Key that will only be made known during runtime, we could work around by Generating the Map.Entry<Key,Value> into a set first (using @IntoSet), then inject into another provider that will decompose into a Map<Key,Value>.

For clearer illustration, below are some runtime randomly generated key,

@Module
class MyModule {
@Provides @IntoSet
static Map.Entry<String, String> entryOne() {
String key = randomStringGenerator();
String value = "Random Value 1";
return new SimpleImmutableEntry(key, value);
}

@Provides @IntoSet
static Map.Entry<String, String> entryTwo() {
String key = randomStringGenerator();
String value = "Random Value 2";
return new SimpleImmutableEntry(key, value);
}
}

This will generate a Set<Map.Entry<String, String>> entries, which we inject into another provider, the decompose the Set into Map as below.

@Module(includes = MyModule.class)
class MyMapModule {
@Provides
static Map<String, String>
randomKeyValueMap(Set<Map.Entry<String, String>> entries) {
Map<String, String> randomKeyValueMap =
new LinkedHashMap<>(entries.size());
for (Map.Entry<String, String> entry : entries) {
randomKeyValueMap.put(entry.getKey(), entry.getValue());
}
return randomKeyValueMap;
}
}

As the above is a workaround, it is not possible to automatically get the binding of Map<Key, Provider<Value>>( or Lazy<Value>), unless the initial set is Set<Map.Entry<Key, Provider<Value>>( or Lazy<Value>). Check out this tutorial if you don’t quite get what this means.

Inherited subcomponent multibindings

One of the value of Multibindings is, the Subcomponent (child) while it can access to the Parent Multibinds dependencies, it (child) also can bind additional dependencies into the Set or Map.

The example code as below

@Module
class ParentModule {
@Provides @IntoSet
static String string1() {
return "parent string 1";
}

@Provides @IntoSet
static String string2() {
return "parent string 2";
}
}
@Module
class ChildModule {
@Provides @IntoSet
static String string3() {
return "child string 3";
}

@Provides @IntoSet
static String string4() {
return "child string 4";
}
}
@Component(modules = ParentModule.class)
interface ParentComponent {
Set<String> strings();
ChildComponent childComponent();
}
@Subcomponent(modules = ChildModule.class)
interface ChildComponent {
Set<String> strings();
}

To test and verify, below shows the Parent contains just 2 elements in the set, while the Child has 4 elements in the set.

@Test
public void testMultibindings() {
Set expectedParentSet = new HashSet<String>
(Arrays.asList("parent string 1", "parent string 2"));
ParentComponent parentComponent =
DaggerParentComponent.create();
assertEquals(2, parentComponent.strings().size());
assertTrue(parentComponent.strings()
.containsAll(expectedParentSet));
Set expectedChildSet = new HashSet<String>(Arrays.asList(
"parent string 1", "parent string 2",
"child string 3", "child string 4"));

ChildComponent childComponent =
parentComponent.childComponent();

assertEquals(4, childComponent.strings().size());
assertTrue(childComponent.strings()
.containsAll(expectedChildSet));
}

Declaring multibindings (for Empty Set/Map)

In the event, the parent component doesn’t have any elements to be inserted into the Set/Map, but there are child component that need to be insert to into it. The parent could use the @Multibinds annotation, and the class need to be abstract.

@Module
abstract class MyModule {
@Multibinds abstract Set<String> aSet();
@Multibinds abstract Map<String, String> aMap();
}

Optionally for set, one could set

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


Thanks for reading. You could check out my other topics here.

Follow me on medium, Twitter, Facebook or Reddit for little tips and learning on mobile development etc related topics. ~Elye~

The Startup

Medium's largest active publication, followed by +524K people. Follow to join our community.

Elye

Written by

Elye

Learning and Sharing Android and iOS Development

The Startup

Medium's largest active publication, followed by +524K people. Follow to join our community.

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade