Skip to content

Multiplatform dependency injection: making Koin, Dagger/Hilt and Swinject work together on Android, iOS and desktop

Different platforms typically use different frameworks for achieving dependency injection. This article shows an approach to integrating dependencies from a shared Kotlin multiplatform module, which uses Koin internally, into three apps:

  1. Desktop app, simply also using Koin;
  2. Android app, using Dagger/Hilt;
  3. iOS app, using Swinject.

This makes integrating multiplatform Kotlin code easier, because you can keep using your existing DI frameworks.

Architecture

Let’s start by looking at an overview of the end result. We’ll have a shared library where we use Koin internally. In a desktop, Android and iOS app we’ll then use that shared code to inject it into the platform-specific DI frameworks and extend it.

Shared library
Shared library
commonMain
commonMain
androidMain
androidMain
Hilt
Hilt
Koin
Koin
iosMain
iosMain
Koin
Koin
Android app
Android app
androidMain
androidMain
Hilt
Hilt
Desktop app
Desktop app
jvmMain
jvmMain
Koin
Koin
Does NOT depend on Koin, only Hilt
Does NOT depend…
iOS app
iOS app
iosMain
iosMain
Swinject
Swinject
Does NOT depend on Koin, only Swinject
Does NOT depend…
Uses Koin
Uses Koin
Exposes Koin dependencies through Hilt
Exposes Koin depende…
Exposes Koin dependencies through a KoinComponent
Exposes Koin dependen…
Text is not SVG – cannot display

Initial code

Our starting code consists of two “Printers” inside a module named [shared]. They simply return a short string and one depends on the other. There’s one called SharedPrinter, which uses the InternalPrinter. That shared one is the one we want to expose to the Android, iOS and desktop sample apps.

package com.example

// Only used in the shared module
internal class InternalPrinter {
    fun print(): String {
        return "Internal printer."
    }
}

// This is the Printer we will later expose to Android/iOS and desktop
class SharedPrinter internal constructor(
    private val internalPrinter: InternalPrinter
) {
    fun print(): String {
        return "Shared printer. ${internalPrinter.print()}"
    }
}

// Provide the dependencies through a Koin module
val sharedModule = module {
    singleOf(::InternalSharedPrinter)
    singleOf(::SharedPrinter)
}

💻 App 1/3: Desktop

We’ll start with the easiest target. In the desktop app we will just use Koin and build upon the module that’s available from the common library. We’ll create a special DesktopPrinter inside desktop/src/jvmMain/kotlin that reuses the printer from the shared module:

internal class DesktopPrinter(
    private val sharedPrinter: SharedPrinter
) {
    fun print(): String {
        return "Desktop printer. ${sharedPrinter.print()}"
    }
}

To let Koin instantiate this, we add a desktopModule where we provide the DesktopPrinter. To make Koin actually capable of injecting the SharedPrinter, we also include the sharedModule from the shared code:

// Desktop-only module to provide our DesktopPrinter
private val desktopModule = module {
    singleOf(::DesktopPrinter)
}

fun main(args: Array<String>) {
    startKoin {
        // Start Koin with both modules
        modules(sharedModule, desktopModule)
    }

    // Now the DesktopPrinter is ready to be retrieved
    val desktopPrinter = get<DesktopPrinter>(DesktopPrinter::class.java)
    print("Test: ${desktopPrinter.print()}")
}

Running this works as expected:

Test: Desktop printer. Shared printer. Internal printer.
Process finished with exit code 0

🤖 App 2/3: Android

That was easy, because we’re simply using Koin in both the shared and desktop-specific code. On Android however, a popular DI framework is Hilt (based on Dagger). How can we make the Koin-provided SharedPrinter available in the Dagger graph?

This is quite simple, because we can write Android-specific code in our shared module and provide a Hilt module in there. To do so, we add both the Koin and Hilt dependencies to the Android source set. Gradle configuration is out of scope for the general idea of this article, so see https://github.com/jacobras/KMP-shared-DI/blob/main/shared/build.gradle.kts for the complete build file.

Now, still in the [shared] module, we’re going to create a small bridge that gets the Koin dependencies and adds them to the Hilt graph. Inside shared/src/androidMain/kotlin:

@Module
@InstallIn(SingletonComponent::class)
class SharedModule {

    // #2: Provide the SharedPrinter into the Hilt graph..
    @Provides
    fun printer(): SharedPrinter {
        // #3: .. by actually retrieving it from the shared Koin module.
        return get(SharedPrinter::class.java)
    }

    companion object {
        fun init(context: Context) {
            // #1: Instantiate Koin first.
            startKoin {
                androidContext(context)
                modules(sharedModule)
            }
        }
    }
}

Moving onto the Android app, we need to start up this module. We do so in our custom application class:

@HiltAndroidApp
internal class MyApplication : Application() {
    override fun onCreate() {
        super.onCreate()
        SharedModule.init(this)
    }
}

We can now again create a platform-specific printer to demonstrate mixing the dependencies coming from both Koin and Hilt:

@Singleton
internal class AndroidPrinter @Inject constructor(
    private val sharedPrinter: SharedPrinter
) {
    fun print(): String {
        return "Android printer. ${sharedPrinter.print()}"
    }
}

Let’s inject this printer in an activity and run it:

@AndroidEntryPoint
class MainActivity : ComponentActivity() {

    @Inject internal lateinit var printer: AndroidPrinter

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        Log.i(MainActivity::class.java.simpleName, "Printer says: ${printer.print()}")
    }
}

This works:

com.example.shared | Printer says: Android printer. Shared printer. Internal printer.

🍎 App 3/3: iOS

Now onto the last app: the iOS one. Here we use Swinject to do our dependency injection. We again start inside our [shared] module, by adding a small bridge inside the iOS source set at shared/src/iosMain/kotlin to allow us to obtain the SharedPrinter in our iOS app:

class SharedDi : KoinComponent {
    init {
        startKoin {
            modules(sharedModule)
        }
    }

    fun sharedPrinter(): SharedPrinter = get()
}

Swinject has a thing called “assemblies”, which act like Dagger/Hilt/Koin’s modules. We create one in the iOS app’s code where we, just like in the Android example, grab dependencies from Koin and provide them to Swinject:

import Foundation
import Swinject
import shared

final class SharedAssembly: Assembly {
    private let sharedDi = SharedDi()

    func assemble(container: Swinject.Container) {
        container.register(SharedPrinter.self) { _ in self.sharedDi.sharedPrinter() }
    }
}

Like in [desktop] and [android], we once again add a platform-specific printer to demonstrate mixing dependencies coming from both Koin and Swinject:

import Foundation
import shared

class SwiftPrinter {
    private let sharedPrinter: SharedPrinter

    init(printer: SharedPrinter) {
        sharedPrinter = printer
    }

    func print() -> String {
        return "Swift printer. " + sharedPrinter.print()
    }
}

Now all that’s left is setting up a Swinject Container, adding the SharedAssembly and then using it:

struct ContentView: View {
    let container = Container()
    let assembler: Assembler

    init() {
        // Include the shared Kotlin dependencies into the Swinject container
        assembler = Assembler([SharedAssembly()], container: container)

        // Include our Swift printer, which uses a dependency from the shared Kotlin module
        container.register(SwiftPrinter.self) { resolver in
            SwiftPrinter(
                printer: resolver.resolve(SharedPrinter.self)!
            )
        }
    }

    var body: some View {
        // Fetch the Swift printer from the container
        let printer = container.resolve(SwiftPrinter.self)!

        VStack {
            Image(systemName: "globe")
                .imageScale(.large)
                .foregroundColor(.accentColor)
            Text(printer.print())
        }
        .padding()
    }
}

Of course, this last example also works:

Swift printer. Shared printer. Internal printer.

Conclusion

As mentioned in the introduction, this is just one approach of doing this. Read the libraries’ documentation and experiment to see what works best in your codebase(s). Thanks to Jimmy Arts for checking if my Swift code (written as an Android engineer) makes sense.

You can reply to this article at https://betterprogramming.pub/multiplatform-dependency-injection-making-koin-dagger-hilt-and-swinject-work-together-on-android-e17b98bd8f7b.