Search code examples
androidkotlindependency-injectiondagger-2

Dagger 2 MissingBinding when swapping concretion for interface


I have two classes that I'm able to have Dagger find and inject for me to use successfully:

TrackEvent

class TrackEvent @Inject constructor(
    private val getTrackingProperties: SomeClass
) : UseCase<Boolean, TrackingEvent> {

    override suspend operator fun invoke(params: TrackingEvent): Boolean {
        return true
    }

SomeClass (note: used as a dependency in TrackEvent)

class SomeClass @Inject constructor() {
    override suspend operator fun invoke(): UserTrackingPropertiesResult {
        return UserTrackingPropertiesResult()
    }
}

TrackEvent has an entry in an @Module annotated interface because it's an implementation of the UseCase interface:

@Component(modules = [MyModule::class])
interface ShiftsComponent {
    fun inject(homeFragment: HomeFragment)
}

@Module
interface MyModule {

    @Binds
    fun bindsTrackEventUseCase(useCase: TrackEvent): UseCase<Boolean, TrackingEvent>
}

Use Case interfaces

interface UseCase<out T, in P> {

    suspend operator fun invoke(params: P): T
}

interface NoParamUseCase<out T> {

    suspend operator fun invoke(): T
}

What I'd like to do is to inject an interface into TrackEvent instead of the concrete SomeClass. So I make SomeClass implement a NoParamUseCase

class SomeClass @Inject constructor(): NoParamUseCase<UserTrackingPropertiesResult> {
    override suspend operator fun invoke(): UserTrackingPropertiesResult {
        return UserTrackingPropertiesResult()
    }
}

update TrackEvent to inject the interface:

class TrackEvent @Inject constructor(
    private val getTrackingProperties: NoParamUseCase<UserTrackingPropertiesResult>) : UseCase<Boolean, TrackingEvent> {

    override suspend operator fun invoke(params: TrackingEvent): Boolean {
        return true
    }
}

…and update MyModule to inform Dagger of which implementation I'd like to use:

@Module
interface MyModule {

    @Binds
    fun bindsTrackEventUseCase(useCase: TrackEvent): UseCase<Boolean, TrackingEvent>

    // New
    @Binds
    fun bindsSomeClass(useCase: SomeClass): NoParamUseCase<UserTrackingPropertiesResult>
}

Dagger now claims that there is a missing binding and that I need to declare an @Provides annotated method:

error: [Dagger/MissingBinding] com.myapp.core.domain.usecase.NoParamUseCase<? extends com.myapp.core.tracking.UserTrackingPropertiesResult> cannot be provided without an @Provides-annotated method.
public abstract interface MyComponent {
                ^
      com.myapp.core.domain.usecase.NoParamUseCase<? extends com.myapp.core.tracking.UserTrackingPropertiesResult> is injected at
          com.myapp.tasks.tracking.domain.usecase.TrackEvent(getTrackingProperties, …)
          …

As far as I can tell, this isn't true:

  • While, I've opted for @Binds in this instance, replacing this with @Provides and manually providing dependencies here yields the same error.
  • I'm using the exact same approach for the TrackEvent class and this works.
  • The only thing I've changed is that I'd like to provide an interface instead. I'd fully understand this error had I not provided the @Binds declaration.

This is different to this question as there's no ambiguity as to which implementation I'm asking Dagger to use in the way that there would be if I had two or more implementations of the same interface.

Why would I get this error now?


Solution

  • According to dagger error message, it expects covariant type NoParamUseCase<? extends UserTrackingPropertiesResult>, but DI module provides invariant NoParamUseCase<UserTrackingPropertiesResult>. To generate appropriate signature for provide method you can change it like this:

    @Binds fun bindsSomeClass(useCase: SomeClass): NoParamUseCase<@JvmWildcard UserTrackingPropertiesResult>
    

    After that your code should be compiled successfully.