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);
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.
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.
@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.
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.