Home Android & Kotlin Books Dagger by Tutorials

14
Multibinding with Maps Written by Massimo Carli

Heads up... You're reading this book for free, with parts of this chapter shown beyond this point as scrambled text.

You can unlock the rest of this book, and our entire catalogue of books and videos, with a raywenderlich.com Professional subscription.

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.

interface InformationPluginSpec {

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

  override fun plugins(): List<InformationPluginSpec> =
      informationPlugins.values.toList() // 2
}
@Module(
    includes = [
      WhereAmIModule::class,
      WeatherModule::class
    ]
)
object InformationSpecsModule
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
  }
  // ...
}
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
  }
}

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.

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
}

Update InformationPluginRegistry with its implementation

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

interface InformationPluginRegistry {
  fun plugins(): List<InformationEndpoint>
}
@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
}

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.

interface InformationEndpoint {

  fun fetchInformation(latitude: Double, longitude: Double): Single<InfoMessage>
}
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>
}
interface WeatherEndpoint : InformationEndpoint {

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

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

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)
    )
  }

Clean up the unused code

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

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
Ceteda 80.5 — Bke Qirsi Ilm dagvn ud eqdekweh

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.

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

  boolean unwrapValue() default true; // 2
}

Using a simple custom @MapKey

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

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

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

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

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.

@MapKey(unwrapValue = false) // 1
annotation class ComplexInfoKey(
    val endpointClass: @JvmSuppressWildcards KClass<out InformationEndpoint>, // 2
    val name: String // 2
)
  implementation "com.google.auto.value:auto-value-annotations:$autovalue_annotation_version"
  kapt "com.google.auto.value:auto-value:$autovalue_version"
@Module
object WeatherModule {

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

  @Provides
  @ApplicationScope
  @IntoMap
  @ComplexInfoKey(
      MyLocationEndpoint::class,
      WHEREAMI_INFO_NAME
  )
  fun provideWhereAmISpec(): InformationPluginSpec = InformationPluginSpec
}
@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
}
Figure 14.3 — The Busso App works as intended
Rorecu 90.9 — Nzo Matfi Uny pastb ij upmatmam

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.

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.

Have feedback to share about the online reading experience? If you have feedback about the UI, UX, highlighting, or other features of our online readers, you can send them to the design team with the form below:

© 2021 Razeware LLC

You're reading for free, with parts of this chapter shown as scrambled text. Unlock this book, and our entire catalogue of books and videos, with a raywenderlich.com Professional subscription.

Unlock Now

To highlight or take notes, you’ll need to own this book in a subscription or purchased by itself.