I'm banging my head against the wall right now because I can't figure this out.
I have a generic Interface called Mapper
which has two generic type parameters. Now I want to leverage multibinding and bind multiple implementations of this interface into a map of type Map<Class<out Any>, Provider<Mapper<Any, Any>>
. My code looks as follows:
interface Mapper<DTO, Entity> {
fun toEntity(model: DTO): Entity
fun toDto(model: Entity): DTO
}
class PersistedIntakeEntryMapper @Inject constructor() : Mapper<PersistedIntakeEntry, IntakeEntry> {
override fun toEntity(model: PersistedIntakeEntry): IntakeEntry { TODO() }
override fun toDto(model: IntakeEntry): PersistedIntakeEntry { TODO() }
}
@Module
interface MapperModule {
@Binds
@IntoMap
@MapperKey(PersistedIntakeEntry::class)
@ModelMappers
fun bindPersistedIntakeEntryMapper(mapper: PersistedIntakeEntryMapper): Mapper<Any, Any>
}
@Singleton
class MapperFactory @Inject constructor(
@ModelMappers val mappers: Map<Class<out Any>, @JvmSuppressWildcards Provider<Mapper<Any, Any>>>,
) {
@Suppress("UNCHECKED_CAST")
inline fun <reified DTO: Any, Entity> get(): Mapper<DTO, Entity>? {
TODO()
}
}
Dagger is specifically complaining that PersistedIntakeEntryMapper
is not assignable to Mapper<Any, Any>
: MapperModule.java:13: error: @Binds methods' parameter type must be assignable to the return type
.
However: the curious thing is that I have the same setup for another component which works like a charm:
interface ViewModelFactory<VM : ViewModel, SavedState, Parameters> {
fun create(savedState: SavedState?, parameters: Parameters?): VM
}
class SetCalorieGoalViewModelFactory @Inject constructor(
private val getCalorieGoalUseCase: GetCalorieGoalUseCase,
private val setCalorieGoalUseCase: SetCalorieGoalUseCase,
private val navigator: Navigator,
) : ViewModelFactory<SetCalorieGoalViewModel, SetCalorieGoalUiState, Nothing> {
override fun create(savedState: SetCalorieGoalUiState?, parameters: Nothing?): SetCalorieGoalViewModel {
TODO()
}
}
@Module
interface SetCalorieGoalUiModule {
@Binds
@IntoMap
@ViewModelKey(SetCalorieGoalViewModel::class)
fun bindSetCalorieGoalViewModelFactory(factory: SetCalorieGoalViewModelFactory)
: ViewModelFactory<ViewModel, Any, Any>
}
I can bind the SetCalorieGoalViewModelFactory
to the ViewModelFactory<SetCalorieGoalViewModel, Any, Any>
type without issue. What is the difference between these setups that makes one of them work and the other one not? I can't figure it out for the life of me. Big thanks in advance to anyone trying to solve this problem.
First of all, check out kotlin documentation on the generic variance topic as well as the related java topics (since dagger generates java code).
Generally the issue is that Mapper<PersistedIntakeEntry, IntakeEntry>
and Mapper<Any, Any>
are invariant, meaning that one is not subtype of the other. Basically this assignment val mapper: Mapper<Any, Any> = PersistedIntakeEntryMapper()
will not compile and that's what dagger tells you. And that makes sense, since Mapper<Any, Any>
must be able to map Any
to Any
and that's obviously not the case with PersistedIntakeEntryMapper
- it expects PersistedIntakeEntry
and IntakeEntry
.
Following the documentation above, it would be possible if your declaration had out
modifier specified like interface Mapper<out DTO, out Entity>
, but that will not work in your case, since you have your type arguments in in
positions.
The interesting question is why it works with ViewModelFactory
. It seems to be a bug in KAPT, it just omits generic type parameters in the generated code when it sees Nothing
. It makes it bypass the compiler checks (but it does not make it safe to use at runtime!), since generics are mostly compile-time things (see type erasure in java).