14
用地图多筹码
由Massimo Carli撰写
在上一章中,您学会了如何使用 与套装多弯曲 通过实现一个简单的框架,将来自远程端点的信息集成到BUSSO应用程序中。通过重构 信息插件框架,您看到如何以简单且声明的方式动态地将功能添加到BusSo。
在本章中,您’ll learn how to use multibinding with 地图
. In particular, you’ll learn how to:
- Configure multibinding with
地图
. - Use fundamental type keys with
@StringKey
,@ClassKey
,@IntKey
and@LongKey
. - 创建一个简单的自定义密钥。
- Use
@KeyMap
to build complex custom keys.
这是一个有机会看到匕首多弯曲如何简化的架构 信息插件框架.
使用带有地图的多元inding
在上一章中,您学会了如何使用 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 信息插件框架, 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 地图
. 地图
is a data structure that allows you map a value to a key.
笔记:如果您想了解更多有关Kotlin中的数据结构以及让您的梦想工作的裂缝访谈,请看看 数据结构&Kotlin的算法.
在以下段落中,您将看到如何使用多indIning 地图<K, V> 关键的地方, K,是以下之一:
-
String
,Int
andLong
Class<T>
- 自定义类型
Using a String
is the simplest case, so you’ll start with that.
使用@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.
在terface InformationPluginSpec {
val informationEndpoint: InformationEndpoint
}
@ApplicationScope
class 信息普通话Impl @Inject constructor(
private val informationPlugins: @JvmSuppressWildcards Map<String, InformationPluginSpec> /// 1
) : 信息普通话 {
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
}
}
使用@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.
简化信息Pluginspec接口
Your first step will be to simplify the InformationPluginSpec
interface. Open InformationPluginspec.kt. 在 plugins.api. 并更改其内容如下:
在terface InformationPluginSpec {
val serviceName: String
}
更新信息普通内容及其实施
接下来,开放 信息普通话 在 plugins.api. and change it to:
在terface 信息普通话 {
fun plugins(): List<InformationEndpoint>
}
@ApplicationScope
class 信息普通话Impl @Inject constructor(
private val retrofit: Retrofit, // 1
informationPlugins: @JvmSuppressWildcards Map<Class<*>, InformationPluginSpec> // 2
) : 信息普通话 {
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.
在terface InformationEndpoint {
fun fetchInformation(latitude: Double, longitude: Double): Single<InfoMessage>
}
在terface 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>
}
在terface WeatherEndpoint : InformationEndpoint {
@GET("${BUSSO_SERVER_BASE_URL}weather/{lat}/{lng}")
override fun fetchInformation(
@Path("lat") latitude: Double,
@Path("lng") longitude: Double
): Single<InfoMessage>
}
为Whereami和天气配置多吲哚
Now it’s time to configure multibinding with 地图
for the information plugins. Open whereamimodule.kt. 在 plugins.whereami.di. 并将其更改为此:
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
您在框架的抽象中更改了一些东西,因此您还需要更改 InformationPluginPresenterimpl.kt. 在 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)
)
}
清理未使用的代码
在建造和运行之前,您可以进行一些清理。删除以下文件您不再需要:
构建并运行BUSSO应用程序
现在,您最终可以构建和运行应用程序并获得图14.2的预期结果:
使用带有自定义密钥的多indIning
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 地图
and a custom type for the key.
@Documented
@Target(ANNOTATION_TYPE) // 1
@Retention(RUNTIME)
public @interface MapKey {
boolean unwrapValue() default true; // 2
}
使用简单的自定义@mapkey
As you read above, @MapKey
annotates the class you use as the key when multibinding with 地图
.
@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
}
}
使用复杂的自定义@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 地图
.
@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 信息普通话Impl @Inject constructor(
private val retrofit: Retrofit,
informationPlugins: @JvmSuppressWildcards Map<ComplexInfoKey, InformationPluginSpec> // 1
) : 信息普通话 {
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
}
关键点
- 匕首allows you to use multibinding with a
地图
. - When you use a
地图
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 the地图
you use in multibinding.