Bridging Koin and Dagger2

In my current project we are slowly migrating from Dagger2 (non-Hilt) into the Koin world, module by module, and figured we need to bridge a couple of new things into the old world. But how exactly would one do that?

My solution was to let Dagger create the Koin graph as part of it’s own graph, like so:

import org.koin.core.module.Module as KoinModule

@Module
class KoinCompatModule {
    @Provides
    @Singleton
    @Suppress("SpreadOperator")
    fun provideKoinApplication(koinModules: Set<KoinModule>): KoinApplication =
        startKoin {
            modules(*koinModules.toTypedArray())
        }
}

Now, each new Gradle module just has to provide a compatible Dagger2 module alongside the Koin module for injection:

import org.koin.core.module.Module as KoinModule

fun coreDomainModule(): KoinModule =
    module {
        single<ClockProvider> { ClockProviderImpl() }
        single<DispatcherProvider> { DispatcherProviderImpl() }
    }

@Module
class CoreDomainModule {
    @Provides
    @IntoSet
    internal fun provideCoreDomainModule(): KoinModule = coreDomainModule()

    @Provides
    internal fun provideClockProvider(app: KoinApplication): ClockProvider =
        app.koin.get()

    @Provides
    internal fun provideDispatcherProvider(app: KoinApplication): DispatcherProvider =
        app.koin.get()
}

In Dagger dependencies are configured unscoped, so that the underlying KoinApplication keeps the ownership / track of instance lifetime. Finally, to have new applications sharing the same modules not depend on Dagger2, we define them as compileOnly:

dependencies {
    compileOnly(libs.dagger2.core)
    ksp(libs.dagger2.compiler)
}

Then all what is missing is to add KoinCompatModule and CoreDomainModule to the specific Dagger component you want to have them in. Obviously all this works best with singleton / unscoped dependencies, i.e. when it comes to activity or even viewmodel-scoped dependencies this will probably not work so easily, but it’s a start 🙂