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
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.
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 Set
s. 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
andLong
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:
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
}
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
andKClass
. - 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 anunwrapValue
attribute with a default value oftrue
, the type of the key is the type of the unique property of your custom key. - A
@KeyMap
is complex if the value for theunwrapValue
attribute isfalse
. 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 theMap
you use in multibinding.