Search code examples
javakotlindependency-injectiondagger-2kapt

Dagger/MissingBinding. Outputter cannot be provided without an @Provides-annotated method


I'm learning Dagger2 via their tutorial but am blocked at part 5. I've implemented everything in Kotlin (jvm) and the logic/behavior matches the tutorial in working order to this point.

However, I'm unable to create SystemOutModule and unsure whether I'm mistaken somewhere with Dagger, or in my translation of their module class.

/**
* Tutorial's Java.
*/
@Module
abstract class SystemOutModule {
    @Provides
    static Outputter textOutputter() {
        return System.out::println;
    }
}
/**
* My Kotlin.
*/
@Module
object SystemOutModule {
    @JvmSuppressWildcards
    @Provides
    fun textOutputter(): (String) -> Unit = { println(it) }
}

The build fails with this error:

~~ ./gradlew clean build


> Task :app:kaptKotlin FAILED
e: /Users/eric/IdeaProjects/atm/app/build/tmp/kapt3/stubs/main/com/es0329/atm/CommandRouterFactory.java:7: error: [Dagger/MissingBinding] com.es0329.atm.Outputter cannot be provided without an @Provides-annotated method.
public abstract interface CommandRouterFactory {
                ^
      com.es0329.atm.Outputter is injected at
          com.es0329.atm.HelloWorldCommand(outputter)
      com.es0329.atm.HelloWorldCommand is injected at
          com.es0329.atm.HelloWorldModule.helloWorldCommand(command)
      com.es0329.atm.Command is injected at
          com.es0329.atm.CommandRouter(command)
      com.es0329.atm.CommandRouter is provided at
          com.es0329.atm.CommandRouterFactory.router()

FAILURE: Build failed with an exception.

Solution

  • Your HelloWorldCommand expects an Outputter, but you are providing a (String) -> Unit instead. There are a few ways to bridge this gap:

    Option 1: Inject the functional type

    Instead of an Outputter, your HelloWorldCommand can ask for a (String) -> Unit directly.

    class HelloWorldCommand @Inject constructor(private val outputter: (String) -> Unit)
    

    This will work, but you might want to provide a different type of (String) -> Unit elsewhere in your app. If you do, you will have to use some sort of @Qualifier.

    Fortunately, this is the only injected lambda in the entire tutorial as far as I can tell, so it won't be a problem here.

    Option 2: Make Outputter a typealias

    You can use a typealias to make Outputter the same type as (String) -> Unit. This works identically to option 1, but is more expressive. Unfortunately, it has the same issue if you ever want to have another (String) -> Unit in your dependency graph later.

    typealias Outputter = (String) -> Unit
    

    Option 3: Provide an implementation of the Outputter interface

    Rather than provide a lambda, you can provide an object implementing Outputter. This is less idiomatic for a simple function, since Kotlin can't assign lambdas directly to SAM interfaces defined in Kotlin.

    @Provides
    fun textOutputter() = object : Outputter {
        override fun output(output: String) = println(output)
    }