Search code examples
javadagger-2kodein

Kodein vs Dagger - Can't Get Dagger Working w/ Multiple Modules


(x-post from /r/androiddev)

I would just like to preface this by saying that this is not a "which is better" post; this is strictly a question about how I can build something using Dagger (and how I built it in Kodein to help illustrate the problem).

I've been using Kodein for a few years now in several work projects, and I've found it to be so easy to work with, that I never look at Dagger anymore. I started a new personal project, and I thought I'd give Dagger another shot.

To keep things simple, I have 3 modules (this is a regular desktop app not an Android one);

  1. app
  2. common
  3. google

app contains a single class App:

class App(
  private val api: GoogleApi,
  private val argParser: ArgParser
) {
  fun run() {
    while(true) {
      api.login(argParser.username, argParser.password);
    }
  }

}

common contains a single class ArgParser (implementation isn't important)

google contains several classes:

class GoogleApi(  
  driveProvider: () -> Drive
) {

  private val drive by lazy {
    driveProvider()
  }

  fun login(username: String, password: String) {
    drive.login() // not real call
  }
}

internal class CredentialRetriever(
  private val transport: NetHttpTransport,
  private val jsonFactory: JacksonFactory
) {

  fun retrieveCredentials() = ...

}

The dependencies for google are:

dependencies {

  implementation "com.google.api-client:google-api-client:$googleApiVersion"

  implementation "com.google.oauth-client:google-oauth-client-jetty:$googleApiVersion"

  implementation "com.google.apis:google-api-services-drive:v3-rev110-$googleApiVersion"

}

I specifically use implementation because I don't want anyone using the underlying Google libraries directly.

To get this to work in Kodein, I do the following in main:

fun main(args: Array<String>) {

  val kodein = Kodein {
    import(commonModule(args = args))
    import(googleModule)
    import(appModule)

    bind<App>() with singleton {
      App(
        api = instance(),
        argParser = instance()
      )
    }
  }

  kodein.direct.instance<App>().run()
}

then in google:

val googleModule = Kodein.Module("Google") {

  bind<CredentialRetriever>() with provider {
    CredentialRetriever(jsonFactory = instance(), transport = instance())
  }

  bind<Drive>() with provider {
    Drive.Builder(
      instance(),
      instance(),
      instance<CredentialRetriever>().retrieveCredentials()
    ).setApplicationName("Worker").build()
  }

  bind<GoogleApi>() with singleton {
    GoogleApi(drive = provider())
  }

  bind<JacksonFactory>() with provider {
    JacksonFactory.getDefaultInstance()
  }

  bind<NetHttpTransport>() with provider{
    GoogleNetHttpTransport.newTrustedTransport()
  }
}

and finally in common:

fun commonModule(args: Array<String>) = Kodein.Module("Common") {
  bind<ArgParser>() with singleton { ArgParser(args = args) }
}

I tried implementing this in Dagger, and couldn't get it to work. My first attempt was to have a Component in app that relied on modules from common and google. This didn't work, because the generated code referenced classes that weren't exposed from google (like Drive). I could've fixed this by making them api dependencies, but I don't want to expose them:

// CredentialRetriever and GoogleApi were updated to have @Inject constructors

// GoogleApi also got an @Singleton

@Module
object GoogleModule {

  @Provides
  internal fun drive(
    transport: NetHttpTransport,
    jsonFactory: JacksonFactory,
    credentialRetriever: CredentialRetreiver
  ): Drive =
    Drive.Builder(
      transport,
      jsonFactory,
      credentialRetriever.retrieveCredentials()
    ).setApplicationName("Worker").build()

  @Provides
  internal fun jsonFactory(): JacksonFactory =
    JacksonFactory.getDefaultInstance()

  @Provides
  internal fun netHttpTransport(): NetHttpTransport = 
    GoogleNetHttpTransport.newTrustedTransport()
}

Next I tried making a component per module (gradle module that is):

// in google module

@Singleton
@Component(modules = [GoogleModule::class])
interface GoogleComponent {
  fun googleApi(): GoogleApi
}

// in common module

@Singleton
@Component(modules = [CommonModule::class])
interface CommonComponent {
  fun argParser(): ArgParser
}

Then in app the fun started:

// results in "AppComponent (unscoped) cannot depend on scoped components:"

@Component(dependencies = [CommonComponent::class, GoogleComponent::class])
interface AppComponent {
  fun app(): App
}

OK so let's make it scoped:

// results in "This @Singleton component cannot depend on scoped components:"

@Singleton
@Component(dependencies = [CommonComponent::class ,GoogleComponent::class])
interface AppComponent {
  fun app(): App
}

EDIT: tried making AppComponent use a custom scope:

// results in "AppComponent depends on more than one scoped component:"

@AppScope
@Component(dependencies = [CommonComponent::class ,GoogleComponent::class])
interface AppComponent {
  fun app(): App
}

How can I achieve this in Dagger? I've read the docs, I think I somewhat understand them, but I have no clue what to do next.


Solution

  • I took the liberty of changing your example a bit to a) remove unnecessary details and b) simplify the setup.

    Given 3 modules with the following classes:

    // ----->> app <<-----
    class App @Inject constructor(
            private val api: AbstractApi,
            private val argParser: ArgParser
    )
    
    // ----->> google <<-----
    // expose a public interface
    interface AbstractApi
    
    // have our internal implementation
    internal class GoogleApi @Inject constructor(
            private val argParser: ArgParser
    ) : AbstractApi
    
    // ----->> common <<-----
    
    // expose some common class
    interface ArgParser
    

    So we need to bind an implementation for ArgParser in both google as well as app. I used ArgParser as an example here how we could pass arguments to our API. GoogleApi is completely internal to make sure nothing leaks. We only expose the interface AbstractApi.

    I made GoogleApi internal to remove the Gradle complexity with implementation / api. The behavior is the same, maybe even a bit more strict: We have some class in our module that we can't expose. This way we have compiler validation as well.

    We can hide all our implementation details behind a component that we add to google to create our GoogleApi implementation for the interface.

    // ----->> google
    @Component(modules = [ApiModules::class])
    interface ApiComponent {
        // has a provision method for our API
        fun api(): AbstractApi
    
        @Component.Factory
        interface Factory {
            // factory method to bind additional args that we need to supply
            fun create(@BindsInstance parser: ArgParser): ApiComponent
        }
    }
    
    @Module
    internal interface ApiModules {
        @Binds
        fun bindApi(googleApi: GoogleApi): AbstractApi
    
    }
    

    We don't use a scope here, because the scope should be handled wherever this component gets used. ArgParser is an example for an argument that we may need to supply to create the object. We could use a @Component.Builder instead of the factory, too.

    Dagger will generate the component within the same module (google), so there won't be any issues about referenced code. All we have to do is retrieve the API in our app module:

    // ----->> app
    @Component(modules = [AppModule::class])
    interface AppComponent {
        fun app(): App
    }
    
    @Module
    class AppModule {
    
        @Provides
        fun provideParser(): ArgParser = object : ArgParser {} // just bind a dummy implementation
    
        @Provides
        fun provideApi(argParser: ArgParser): AbstractApi {
            return DaggerApiComponent.factory().create(argParser).api()
        }
    }
    

    We can now use the component factory to create an instance from our module. If we need a scope we can add it as usual on the @Provides method.

    This setup should completely hide any detail from the app module behind the public interface. The generated code resides within the same module.


    Why not expose a @Module? A @Subcomponent?

    As reported, adding a module to a component will generate the factory code within that component as well, which will try to use non-referenced classes. The same would apply to a subcomponent.

    Why not use a Component Dependency?

    Since there is no scope on the component we might as well add it as a component dependency, but we wouldn't be able to add a scope then. Also we'd have a harder time passing in arguments, since we'd have to supply them when creating the component.