I have the following definitions:
@Module
class WeaverDataModule {
// Provide the three pumps from providers
// All of them still explicitly mark 'Pump' as their return type
@Provides @IntoSet fun providesPump(thermosiphon: Thermosiphon) : Pump = thermosiphon
@Provides @IntoSet fun providesAnotherPump(suctionBased: SuctionBased) : Pump = suctionBased
@Provides @IntoSet fun providesGenericPump(genericPump: GenericPump) : Pump = genericPump
}
@Component(modules = [WeaverDataModule::class])
interface WeaverData {
// Get the CoffeeMaker
fun coffeeMaker(): CoffeeMaker
// Get the list of pumps
fun getPumps() : Set<Pump>
}
interface Pump
// The three pumps
class Thermosiphon @Inject constructor(val heater: Heater) : Pump
class SuctionBased @Inject constructor() : Pump
class GenericPump @Inject constructor() : Pump
// Some random heater
class Heater @Inject constructor()
In my code, when I do the following:
val cm = DaggerWeaverData.builder().build().getPumps()
I do get the three pumps as expected. However, when I'm trying to inject it into some other class:
class CoffeeMaker @Inject constructor(
private val heater: Heater,
private val pump: Set<Pump>
) {
fun makeCoffee() =
"Making coffee with heater ${heater::class.java} and using pumps" +
" ${pump.map { it::class.java }.joinToString(",")}"
}
I get the following error:
e: .../WeaverData.java:7: error: [Dagger/MissingBinding] java.util.Set<? extends weaver.Pump> cannot be provided without an @Provides-annotated method.
public abstract interface WeaverData {
^
java.util.Set<? extends weaver.Pump> is injected at
weaver.CoffeeMaker(…, pump)
weaver.CoffeeMaker is provided at
weaver.WeaverData.coffeeMaker()
I've tried injecting Collection<Pump>
also, but I still get a similar error. In the dagger docs on multibinding, the example (in Java) shows the following:
class Bar {
@Inject Bar(Set<String> strings) {
assert strings.contains("ABC");
assert strings.contains("DEF");
assert strings.contains("GHI");
}
}
which is exactly what I'm doing. And for constructor-based injection, it is working just fine in Kotlin, because the following compiles and runs as expected:
class CoffeeMaker @Inject constructor(
private val heater: Heater
) {
fun makeCoffee() =
"Making coffee with heater ${heater::class.java}"
}
So I'm kind of at a loss on how do I get this multibinding to work.
So turns out what you need to do is:
class CoffeeMaker @Inject constructor(
private val heater: Heater,
private val pumps: Set<@JvmSuppressWildcards Pump>
) {
fun makeCoffee() =
"Making coffee with heater ${heater::class.java} with pumps ${pumps.map { it::class.java }.joinToString(",")}"
}
This is because Set
is defined in Kotlin as Set<out E>
which translates into Java as Set<? extends Pump>
. From a type-theory perspective, Set<? extends Pump>
is different from Set<Pump>
and hence Dagger (probably) refuses to see Set<Pump>
as an injectable for Set<? extends Pump>
, which is fair and the right behavior.
The problem we have is that for any of these collections, since they are immutable by default, a declaration of type Set<X>
will translate to Set<? extends X>
, as an immutable collection only has references to the resolved type on returns and is hence covariant. To verify this theory, the following also works:
class CoffeeMaker @Inject constructor(
private val heater: Heater,
private val pumps: MutableSet<Pump>
) {
fun makeCoffee() =
"Making coffee with heater ${heater::class.java} with pumps ${pumps.map { it::class.java }.joinToString(",")}"
}
Note the use of MutableSet
, which is defined as MutableSet<E> : Set<E> ...
. This is probably not something one should use because I doubt that this set is actually mutable. So what we do need is for the kotlin compiler to treat Set<out E>
as Set<E>
(the assignabiliy is valid in this case, just not the other way around). So do so, we use the @JvmSuppressWildcards
annotation. I hope this helps somebody else facing similar issues.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With