Chapters

Hide chapters

Dagger by Tutorials

First Edition · Android 11 · Kotlin 1.4 · AS 4.1

14. Multibinding With Maps
Written by Massimo Carli

In the previous chapter, you learned how to use multibinding with Set by implementing a simple framework to integrate information from remote endpoints into the Busso App. By refactoring the Information Plugin Framework, you saw how to dynamically add features to Busso in a simple and declarative way.

Figure 14.1 - Information Plugin Framework
Figure 14.1 - Information Plugin Framework

In this chapter, you’ll learn how to use multibinding with Map. In particular, you’ll learn how to:

  • Configure multibinding with Map.
  • Use fundamental type keys with @StringKey, @ClassKey, @IntKey and @LongKey.
  • Create a simple custom key.
  • Use @KeyMap to build complex custom keys.

This is an opportunity to see how Dagger multibinding simplifies the architecture of the Information Plugin Framework.

Using multibinding with Map

In the previous chapter, you learned how to use multibinding with Sets. Set is an unordered data structure that lets you avoid duplicates. This is great but sometimes you might need something different. For instance, in the case of the Information Plugin Framework, a Set doesn’t allow you to decide the order of the information to display on the screen.

In any case, Dagger offers you another option: multibinding with Map. Map is a data structure that allows you map a value to a key.

Note: If you want to know more about data structures in Kotlin and crack interviews for getting your dream job, have a look at Data Structures & Algorithms in Kotlin.

In the following paragraph, you’ll see how to use multibinding with Map<K, V> where the key, K, is one of the following:

  • String,Int and Long
  • Class<T>
  • Custom type

Using a String is the simplest case, so you’ll start with that.

Using @StringKey

For your first example, suppose you want to simplify InformationPluginSpec by removing the property name and giving the plugin a name when you add it to the registry.

To do this, open InformationPluginSpec.kt in plugins.api and remove serviceName, so it looks like this:

interface InformationPluginSpec {

  val informationEndpoint: InformationEndpoint
}

Now, open InformationPluginRegistryImpl.kt in plugins.di and change it like this:

@ApplicationScope
class InformationPluginRegistryImpl @Inject constructor(
    private val informationPlugins: @JvmSuppressWildcards Map<String, InformationPluginSpec> /// 1
) : InformationPluginRegistry {

  override fun plugins(): List<InformationPluginSpec> =
      informationPlugins.values.toList() // 2
}

In this code, you:

  1. Replaced Set<InformationPluginSpec> with Map<String, InformationPluginSpec>.
  2. Returned List<InformationPluginSpec> from the values in Map<String, InformationPluginSpec>.

Now, you’ve set everything up and just need to register InformationPluginSpec in InformationPluginRegistry. After the changes you made above, you need to remove the definition in InformationSpecsModule.kt in plugins.di. So open that file and change it like this:

@Module(
    includes = [
      WhereAmIModule::class,
      WeatherModule::class
    ]
)
object InformationSpecsModule

Note: You could also delete it and restore the state of the app before More about Multibinding with Set: @ElementsIntoSet. However, simply deleting the previous definition is fine.

Next, open WeatherModule.kt in plugins.weather.di and apply the following change:

const val WEATHER_INFO_NAME = "Weather"
@Module(includes = [WeatherModule.Bindings::class])
object WeatherModule {
  @Provides
  @ApplicationScope
  @IntoMap // 1
  @StringKey(WEATHER_INFO_NAME) // 2
  fun provideWeatherSpec(endpoint: WeatherInformationEndpoint): InformationPluginSpec = object : InformationPluginSpec {
    override val informationEndpoint: InformationEndpoint
      get() = endpoint
  }
  // ...
}

As you can see in this code, you use:

  1. @IntoMap to tell Dagger that the object you @Provides is part of a multibinding definition with a Map.
  2. @StringKey(WEATHER_INFO_NAME) to tell Dagger that the key of the object you @Provides is a String and, in this case, its value matches WEATHER_INFO_NAME.

Now, open WhereAmIModule.kt in plugins.whereami.di and replace the code with:

const val WHEREAMI_INFO_NAME = "WhereAmI"
@Module(includes = [WhereAmIModule.Bindings::class])
object WhereAmIModule {
  // ...
  @Provides
  @ApplicationScope
  @IntoMap
  @StringKey(WHEREAMI_INFO_NAME)
  fun provideWhereAmISpec(endpoint: WhereAmIEndpointImpl): InformationPluginSpec = object : InformationPluginSpec {
    override val informationEndpoint: InformationEndpoint
      get() = endpoint
  }
}

You can now successfully build and run as usual.

Note: Dagger also allows keys of type Int and Long. You can easily try them out on your own by using @IntKey and @LongKey and repeating what you just did for @StringKey.

Using @StringKey, you just set the name of the information plugin as the key of the Map you’re using to multibind. In this case, you’re not actually using that information, but you’ll see how to use key information in a more complex example next.

Using @ClassKey

Another type of key Dagger gives you is Class<T>. You can use it the same way you used String, but for your next step, you’ll try something more ambitious, instead.

You’ll use code that’s similar to the information plugins you implemented earlier. The main difference is in InformationEndpoint — the rest is just scaffolding.

To do this you basically need to:

  1. Simplify the InformationPluginSpec interface
  2. Update InformationPluginRegistry with its implementation
  3. Use InformationEndpoint as abstraction for the information plugin endpoints
  4. Configure multibindings for WhereAmI and Weather
  5. Migrate InformationPluginPresenterImpl to the new abstractions
  6. Clean up the unused code
  7. Build and run the Busso app

Simplifying the InformationPluginSpec interface

Your first step will be to simplify the InformationPluginSpec interface. Open InformationPluginSpec.kt in plugins.api and change its content like this:

interface InformationPluginSpec {
  val serviceName: String
}

Basically, InformationPluginSpec is just a name now.

Update InformationPluginRegistry with its implementation

Next, open InformationPluginRegistry in plugins.api and change it to:

interface InformationPluginRegistry {
  fun plugins(): List<InformationEndpoint>
}

Here, you’re just trying to get a list of InformationEndpoints, and that’s what InformationPluginRegistry provides. The magic is in its implementation.

Now, open InformationPluginRegistryImpl.kt in plugins.impl and change it like this:

@ApplicationScope
class InformationPluginRegistryImpl @Inject constructor(
    private val retrofit: Retrofit, // 1
    informationPlugins: @JvmSuppressWildcards Map<Class<*>, InformationPluginSpec> // 2
) : InformationPluginRegistry {

  val endpoints = informationPlugins.keys.map { clazz ->
    retrofit.create(clazz) // 3
  }.map { endpoint ->
    endpoint as InformationEndpoint
  }.toList()

  override fun plugins(): List<InformationEndpoint> = endpoints // 4
}

In this code, you:

  1. Add Retrofit as the primary constructor parameter. This is the object you need to create the actual InformationEndpoint.
  2. Receive a Map<Class<*>, InformationPluginSpec> from Dagger. You’ll see how this works very soon.
  3. Initialize an endpoints variable of the type InformationEndpoint using Retrofit and the information about the multibinding that Dagger provides.
  4. Return endpoints as List<InformationEndpoint>.

Because of 4 you need all the endpoint be an implementation of the InformationEndpoint interface.

Use InformationEndpoint as abstraction for the information plugin endpoints

Look at MyLocationEndpoint and WeatherEndpoint and notice there’s a problem — they don’t share any abstraction. The implementations Retrofit creates for you aren’t InformationEndpoint implementations, and they all define operations with different names and parameters.

To fix this, open InformationEndpoint.kt in plugins.api and change it like this:

interface InformationEndpoint {

  fun fetchInformation(latitude: Double, longitude: Double): Single<InfoMessage>
}

This gives you two parameters that are consistent with the specific Retrofit interfaces.

Now, change MyLocationEndpoint.kt in plugins.whereami.endpoint like this:

interface MyLocationEndpoint : InformationEndpoint { // 1
  @GET("${BUSSO_SERVER_BASE_URL}myLocation/{lat}/{lng}")
  override fun fetchInformation( // 2
      @Path("lat") latitude: Double,
      @Path("lng") longitude: Double
  ): Single<InfoMessage>
}

Here you:

  1. Added the explicit implementation of the InformationEndpoint interface.
  2. Renamed whereAmIInformation() in fetchInformation() to be consistent with InformationEndpoint and added the override keyword.

Now, open WeatherEndpoint.kt in plugins.weather.endpoint and do the same, getting the following code:

interface WeatherEndpoint : InformationEndpoint {

  @GET("${BUSSO_SERVER_BASE_URL}weather/{lat}/{lng}")
  override fun fetchInformation(
      @Path("lat") latitude: Double,
      @Path("lng") longitude: Double
  ): Single<InfoMessage>
}

You have the InformationEndpoint implementations for MyLocationEndpoint and WeatherEndpoint now. You can finally configure multibinding.

Configure multibindings for WhereAmI and Weather

Now it’s time to configure multibinding with Map for the information plugins. Open WhereAmIModule.kt in plugins.whereami.di and change it to this:

const val WHEREAMI_INFO_NAME = "WhereAmI"
@Module
object WhereAmIModule {
  @Provides
  @ApplicationScope
  @IntoMap // 1
  @ClassKey(MyLocationEndpoint::class) // 2
  fun provideWhereAmISpec():
      InformationPluginSpec = object : InformationPluginSpec { // 3
    override val serviceName: String
      get() = WHEREAMI_INFO_NAME
  }
}

Here, you:

  1. Use @IntoMap to tell Dagger to put the object you @Provide in a Map with values of type InformationPluginSpec.
  2. Use @ClassKey(MyLocationEndpoint::class) to tell Dagger that the key is a Class<T>, where T is MyLocationEndpoint.
  3. Remove endpoint, which you don’t need anymore because the same information is in the @ClassKey attribute.

More importantly, WhereAmIModule contains just one definition. The others are no longer necessary because the registry now creates the endpoints.

Next, switch to WeatherModule.kt in plugins.weather.di and change it to the following:

const val WEATHER_INFO_NAME = "Weather"
@Module
object WeatherModule {
  @Provides
  @ApplicationScope
  @IntoMap // 1
  @ClassKey(WeatherEndpoint::class) // 2
  fun provideWeatherSpec():
      InformationPluginSpec = object : InformationPluginSpec { // 3
    override val serviceName: String
      get() = WEATHER_INFO_NAME
  }
}

As done in the case of WhereAmIModule you here:

  1. Use @IntoMap to tell Dagger to put the object you @Provide in a Map with values of type InformationPluginSpec.
  2. Use @ClassKey(WeatherEndpoint::class) to tell Dagger that the key is a Class<T>, where T is WeatherEndpoint.
  3. Remove endpoint, which you don’t need anymore because the same information is in the @ClassKey attribute.

Now you’re ready to use the new configurations.

Migrate InformationPluginPresenterImpl to the new abstractions

You changed something in the abstraction for the framework, so you also need to change InformationPluginPresenterImpl.kt in plugins.ui. Just replace the start() implementation with the following:

  override fun start() {
    disposables.add(
        locationObservable.filter(::isLocationEvent)
            .map { locationEvent ->
              locationEvent as LocationData
            }
            .firstElement()
            .map { locationData ->
              val res = informationPluginRegistry.plugins().map { endpoint ->
                val location = locationData.location
                endpoint.fetchInformation(location.latitude, location.longitude) // HERE
                    .toFlowable()
              }
              Flowable
                  .merge(res)
                  .collectInto(mutableListOf<String>()) { acc, item ->
                    acc.add(item.message)
                  }
            }
            .subscribe(::manageResult, ::handleError)
    )
  }

What you get from the InformationPluginRegistry and the way you invoke the InformationEndpoint are now different.

Clean up the unused code

Before building and running, you have some cleanup to do. Delete the following files you don’t need anymore:

  • WeatherInformationEndpoint
  • WeatherInformationEndpointImpl
  • WhereAmIEndpoint
  • WhereAmIEndpointImpl

Now you’re ready to test the Busso app.

Build and run the Busso app

Now you can finally build and run the app and get the expected result in Figure 14.2:

Figure 14.2 — The Busso App works as intended
Figure 14.2 — The Busso App works as intended

The code is now much simpler, and you’ve removed most of the classes and interfaces from the first implementation of the framework.

The question now is: Can you do even better? You’ll make more improvements in the next section.

Using multibinding with a custom Key

You can usually cover all the use cases you encounter by using @StringKey and @ClassKey. But just in case you need something special, Dagger offers multibinding with Map and a custom type for the key.

This is useful when you want to give Dagger additional information when you implement the multibindings, as you’ll see also when you’ll learn how Dagger works with Google Architecture Components. In this scenario, Dagger provides the following @MapKey annotation:

@Documented
@Target(ANNOTATION_TYPE) // 1
@Retention(RUNTIME)
public @interface MapKey {

  boolean unwrapValue() default true; // 2
}

There are some important things to understand here:

  1. @MapKey is an annotation for annotations. This is the annotation you’ll use to mark the type you’ll use as the key for the multibinding.
  2. It has an attribute, unwrapValue, that has a default value of true. You’ll learn more about it soon, but in short, it tells Dagger whether or not the key is complex.

A code example will help you to better understand how all this works.

Using a simple custom @MapKey

As you read above, @MapKey annotates the class you use as the key when multibinding with Map.

As an example, suppose you want to do what you did with @KeyClass — provide Class<InformationEndpoint> for the endpoint of an information plugin. But this time, you want to use a custom type as the key.

The first step is to create a new file named SimpleInfoKey.kt in plugins.api with the following code:

@MapKey // 1
annotation class SimpleInfoKey(
    val endpointClass: KClass<*> // 2
)

In this very simple code, you:

  1. Use @MapKey to annotate the custom @SimpleInfoKey annotation.
  2. Define endpointClass as the only property of type KClass<*>.

Next, you need to replace the current @ClassKey with the custom annotation @SimpleInfoKey. Open WeatherModule.kt in plugins.weather.di and apply the following change:

@Module
object WeatherModule {

  @Provides
  @ApplicationScope
  @IntoMap
  @SimpleInfoKey(WeatherEndpoint::class) // HERE
  fun provideWeatherSpec(): InformationPluginSpec = object : InformationPluginSpec {
    override val serviceName: String
      get() = WEATHER_INFO_NAME
  }
}

You just replaced @ClassKey with @SimpleInfoKey, keeping the same WeatherEndpoint::class value for the attribute.

Now, open WhereAmIModule.kt in plugins.whereami.di and do the same thing:

@Module
object WhereAmIModule {

  @Provides
  @ApplicationScope
  @IntoMap
  @SimpleInfoKey(MyLocationEndpoint::class) // HERE
  fun provideWhereAmISpec(): InformationPluginSpec = object : InformationPluginSpec {
    override val serviceName: String
      get() = WHEREAMI_INFO_NAME
  }
}

And that’s it. You don’t have to do anything except build and run the app and it will work as expected.

When you use @MapKey to define a custom key that lets you use multibinding with Map, and unwrapValue() has a default value of true, it means that:

  • Your key can only have a single property. In the previous example, it was endpointClass.
  • The type for the key is same as the type for that single property — in this case, KClass<*>. That’s why you didn’t have to change InformationPluginRegistryImpl.

For your final task, you’ll create a more complex key.

Using a complex custom @MapKey

Your final step is to make the InformationPluginSpec definition redundant and instead, put all the information you need in a custom key to use with multibinding and Map.

Start by creating a new file named ComplexInfoKey.kt in plugins.api and adding:

@MapKey(unwrapValue = false) // 1
annotation class ComplexInfoKey(
    val endpointClass: @JvmSuppressWildcards KClass<out InformationEndpoint>, // 2
    val name: String // 2
)

This code has some interesting parts. Here, you:

  1. Use @MapKey to pass false as the value for unwrapValue.
  2. Define two different properties with the types KClass<out InformationEndpoint> and String.

In this case, the annotation has false as unwrapValue’s value, which means that the type for the key is now the annotation itself.

It’s important to say that, if you want to use a complex @MapKey with false for the unwrapValue attribute, Dagger requires an additional dependency.

Add it by opening build.gradle for the main app module and adding the following definitions in the dependencies block:

  implementation "com.google.auto.value:auto-value-annotations:$autovalue_annotation_version"
  kapt "com.google.auto.value:auto-value:$autovalue_version"

In this code you:

  1. Add the dependency to the auto-value-annotations library
  2. Configure the annotation processor the auto value library needs for the code generation.

Note: The values for the version parameters are already available in versions.gradle in the root of the project.

Now, open WeatherModule.kt in plugins.weather.di and apply the following change:

@Module
object WeatherModule {

  @Provides
  @ApplicationScope
  @IntoMap
  @ComplexInfoKey( // 1
      WeatherEndpoint::class,
      WEATHER_INFO_NAME
  )
  fun provideWeatherSpec(): InformationPluginSpec = InformationPluginSpec
}

In this code, you:

  1. Use @ComplexInfoKey as the key, passing it InformationEndpoint’s information and the name of the plugin.
  2. Return InformationPluginSpec — which you’re only using now to put something into the Map for this exercise.

For this reason, you now need to open InformationPluginSpec.kt in plugins.api and make the following changes:

object InformationPluginSpec

Now, open WhereAmIModule.kt in plugins.whereami.di and apply the same change:

@Module
object WhereAmIModule {

  @Provides
  @ApplicationScope
  @IntoMap
  @ComplexInfoKey(
      MyLocationEndpoint::class,
      WHEREAMI_INFO_NAME
  )
  fun provideWhereAmISpec(): InformationPluginSpec = InformationPluginSpec
}

When you use a complex key like this, you also need to change Map’s type, which now becomes the annotation type.

Do this by opening InformationPluginRegistryImpl.kt in plugins.impl and applying the following changes:

@ApplicationScope
class InformationPluginRegistryImpl @Inject constructor(
    private val retrofit: Retrofit,
    informationPlugins: @JvmSuppressWildcards Map<ComplexInfoKey, InformationPluginSpec> // 1
) : InformationPluginRegistry {

  val endpoints = informationPlugins.keys.map { complexKey ->
    retrofit.create(complexKey.endpointClass.java as Class<*>) // 2
  }.map { endpoint ->
    endpoint as InformationEndpoint
  }.toList()

  override fun plugins(): List<InformationEndpoint> = endpoints
}

In this code, you:

  1. Make ComplexInfoKey the type for the key in the Map you inject as the primary constructor parameter.
  2. Use ComplexInfoKey’s endpointClass, which you have as a key.

Now you can finally build and successfully run the Busso App.

Figure 14.3 — The Busso App works as intended
Figure 14.3 — The Busso App works as intended

Key points

  • Dagger allows you to use multibinding with a Map.
  • When you use a Map for multibinding, you can use keys of the following types: String, Int, Long and KClass.
  • If you need more informative keys, @KeyMap allows you to create custom types, which you can use in a simple or complex way.
  • If you use @KeyMap and an unwrapValue attribute with a default value of true, the type of the key is the type of the unique property of your custom key.
  • A @KeyMap is complex if the value for the unwrapValue attribute is false. In this case, you need to add an auto-value as a dependency in your project.
  • You must use a complex @KeyMap for the key of the Map you use in multibinding.

Wow! It’s been a very interesting chapter. Using multibinding with Map, you managed to improve the Information Plugin Framework architecture even further.

But, what if you want a more structured and organized set of modules? In the next chapter, you’ll learn how Dagger can help you.

Have a technical question? Want to report a bug? You can ask questions and report bugs to the book authors in our official book forum here.
© 2026 Kodeco Inc.