Multibindings in Dagger2

One of the advanced features Dagger2 gives us is multibindings. Multibindings allow us to provide multiple dependencies in a single collection, even when these dependencies are in different modules. Multibindings have a variety of different use cases which should become apparent after we take a look at some code. If you have no idea what Dagger2 is, I would recommend reading my introduction to the library here.

Multibinding with @IntoSet

Let’s say we wish to have a Set of dependencies (a collection containing no duplicates) be injectable, we can achieve this quite easily using the @IntoSet annotation.

@Module
class ModuleA{

    @Provides
    @IntoSet
    fun provideStringA(): String = "A"
}

Here we have a standard module which is providing a String, “A”. The only difference is that we are also annotating the providing method with @IntoSet. This annotation will tell Dagger to add this String to a generically typed Set, Set<String>. How does Dagger know to add it to a Set<String>? Because we are returning a String.

We can add more String dependencies into our Set by following the exact steps taken above.

@Module
class ModuleB{

    @Provides
    @IntoSet
    fun provideStringB() = "B"
}

So now that we have two modules which are providing two Strings annotated with @IntoSet, this means that our component will now construct a Set containing these two Strings.

Taking advantage of constructor injection, we can inject the Set like this:

class SomeClass @Inject constructor(stringSet: Set<String>)

And the necessary component:

@Component(modules = [ModuleA::class, ModuleB::class])
interface AppComponent {

    fun buildSomeClass(): SomeClass
}

We will now have access to all the String dependencies we marked (in their providing methods) with @IntoSet. Of course, this works with any type – the usage of String here is just for example purposes. All providing functions returning specific types will be placed in their own Sets (eg, modules returning Int will be put in a Set<Int> and so on).

Multiple Elements

It’s also possible to add multiple elements to our Set at once, instead of adding one at a time. We can do this by using the @ElementsIntoSet annotation. The process is quite similar to before.

@Module
class ModuleC{

    @Provides
    @ElementsIntoSet
    fun provideStringsC(): Set<String> = HashSet(listOf("C", "D"))
}

Instead of returning a single dependency, we are now returning a Set which will be added to the component. If we include this module in our component, we will now have the Strings [“A”, “B”, “C”, “D”] instead of [“A”, “B”] inside SomeClass.

Multibinding with @IntoMap

Similar to @IntoSet, we can use the @IntoMap annotation to include a collection of dependencies inside a Map. The difference here is that we also need to designate a key (since we are putting the dependencies in a Map). This will make sense after taking a look at some code.

@Module
class ModuleA{

    @Provides
    @IntoMap
    @StringKey("Test")
    fun provideStringA() = "A"
}

Here’s our previous ModuleA class using @IntoMap instead of @IntoSet. We also include the use of the @StringKey annotation, giving it the String “Test”. This means that for this dependency, our key will be “Test” and the value will be “A”. Simple, right? There exist other type keys out of the box in Dagger, including @IntKey, @ClassKey and @LongKey.

The steps to include this map in our project is more or less the same as when we were working with our Set. Here’s how the code could look:

class SomeClass @Inject constructor(stringMap: Map<String, String>)
@Component(modules = [ModuleA::class, ModuleB::class])
interface AppComponent {

    fun buildSomeClass(): SomeClass
}

Since we are using a String for a key and returning a String for the providing function inside ModuleA, our Map will be a Map<String, String>.

ModuleB looks like this:

@Module
class ModuleB{

    @Provides
    @IntoMap
    @StringKey("Test2")
    fun provideStringsB() = "B"
}

Notice we are using a different key for this dependency; this is required since a Map cannot have duplicate keys. If you end up using a duplicate key, for the same generically typed key-value pair, Dagger will throw a compile error. We can use the same key for different key-value pairs however. If we rewrote ModuleB to look like the following:

@Module
class ModuleB{

    @Provides
    @IntoMap
    @StringKey("Test")
    fun provideStringsB() = 1
}

Our project would compile without issue. Since we are now basically giving Dagger a new key-value pair of <String, Int>, by returning an Int instead of a String with the providing method.

If you were curious, here’s what the code looks like for the @StringKey annotation both in Java and Kotlin respectively:

@Documented
@Target(METHOD)
@Retention(RUNTIME)
@MapKey
public @interface StringKey {
  String value();
}
@Target(AnnotationTarget.FUNCTION, AnnotationTarget.PROPERTY_GETTER, AnnotationTarget.PROPERTY_SETTER)
@Retention(AnnotationRetention.RUNTIME)
@MapKey
annotation class StringKey(val value: String)

Custom Map keys

If you would like to give your Map a key that is not already included, you can easily create your own. Let’s say we want to have any child of ViewModel (or any other class) be a class key, we can easily achieve this by writing the following annotation:

@Target(AnnotationTarget.FUNCTION, AnnotationTarget.PROPERTY_GETTER, AnnotationTarget.PROPERTY_SETTER)
@Retention(AnnotationRetention.RUNTIME)
@MapKey
annotation class ViewmodelKey(val value: KClass<out ViewModel>)

If we were to apply this new key to one of our existing modules, it would look like the following:

@Module
class ModuleA{

    @Provides
    @IntoMap
    @ViewmodelKey(SomeViewmodel::class)
    fun provideStringA() = "A"
}

The key which will give us String “A” is now the SomeViewmodel class.

Complex Map Keys

Having a key which takes more than one annotation member is also possible. Doing this requires us to set MapKey’s unwrapValue to false.

@Target(AnnotationTarget.FUNCTION, AnnotationTarget.PROPERTY_GETTER, AnnotationTarget.PROPERTY_SETTER)
@Retention(AnnotationRetention.RUNTIME)
@MapKey(unwrapValue = false)
annotation class ViewmodelKey(val value: KClass<out ViewModel>, val value2: String, val value3: Int)

The key will now take a Class which inherits from Viewmodel, a String and an Int. Using this key would look like the following:

@Module 
class ModuleA {
 
@Provides 
@IntoMap 
@ViewmodelKey(SomeViewmodel::class, "Test", 1) 
fun provideStringsB() = "A" 
}

However, the key-value pair for our Map will no longer be Map<KClass<out Viewmodel>, String>. It now becomes Map<ViewmodelKey, String>. Accessing this map via constructor injection would look like this:

class SomeClass @Inject constructor(map: Map<ViewModelKey, String>)

Taking advantage of multibindings lets us do some pretty cool stuff, as you will see in later articles.

Liked the article? Share it!

Leave a Reply

avatar

This site uses Akismet to reduce spam. Learn how your comment data is processed.

  Subscribe  
Notify of