Back to Blog

Mastering Dependency Injection in Kotlin Multiplatform with Koin

Dependency Injection (DI) is a cornerstone of clean, testable mobile architectures. While Android developers have long relied on Dagger and Hilt, these frameworks depend heavily on Java annotation processors (APT/KSP) and JVM bytecode generation. Consequently, they cannot be used in a shared Kotlin Multiplatform (KMP) module that targets iOS native code.

Fortunately, Koin—a pragmatic, lightweight, and pure-Kotlin dependency injection framework—has emerged as the standard DI solution for Kotlin Multiplatform.


1. Adding Koin Dependencies

To configure Koin, open your shared module's build.gradle.kts and declare the multiplatform dependencies:

// shared/build.gradle.kts

kotlin {
    sourceSets {
        commonMain.dependencies {
            // Core Koin dependency for multiplatform logic
            implementation("io.insert-koin:koin-core:3.5.6")
        }
        androidMain.dependencies {
            // Android-specific Koin extensions (like viewmodel injecting and context helper)
            implementation("io.insert-koin:koin-android:3.5.6")
        }
    }
}

2. Setting Up Common Modules

In commonMain, group your dependencies into modules just like you would in a native Android app. We can define our API client, local database, repositories, and ViewModels:

// commonMain/kotlin/com/example/portfolio/di/CommonModule.kt

import org.koin.dsl.module
import io.ktor.client.*
import com.example.portfolio.feed.*

val networkModule = module {
    single<HttpClient> { 
        HttpClient { /* Ktor configuration here */ } 
    }
}

val databaseModule = module {
    // Platform-specific databases will be injected here
}

val repositoryModule = module {
    single<FeedRepository> { FeedRepositoryImpl(get(), get()) }
}

val viewModelModule = module {
    // In KMP, we can register viewmodels as factories
    factory { FeedViewModel(get()) }
}

// Combine all common modules
val commonModules = listOf(
    networkModule,
    databaseModule,
    repositoryModule,
    viewModelModule
)

3. Initializing Koin on Android

On Android, we need to pass the application context to Koin so it can resolve Android-specific dependencies. We initialize Koin in our custom Application class:

// androidMain/kotlin/com/example/portfolio/AndroidApp.kt

import android.app.Application
import org.koin.android.ext.koin.androidContext
import org.koin.android.ext.koin.androidLogger
import org.koin.core.context.startKoin
import com.example.portfolio.di.commonModules

class AndroidApp : Application() {
    override fun onCreate() {
        super.onCreate()

        startKoin {
            // Log Koin operations to logcat
            androidLogger()
            // Provide Android context
            androidContext(this@AndroidApp)
            // Load modules
            modules(commonModules)
        }
    }
}

4. Initializing Koin on iOS

Since iOS does not have an Application class or standard Context, we initialize Koin using a helper function written in iosMain that the Swift App Delegate can call when the app launches:

// iosMain/kotlin/com/example/portfolio/di/KoinIOS.kt

import org.koin.core.context.startKoin
import org.koin.core.KoinApplication

fun initKoin(): KoinApplication {
    return startKoin {
        modules(commonModules)
    }
}

Then, inside your Swift entry point (App.swift or AppDelegate.swift), call this initialization helper:

// iosApp/iosApp/iOSApp.swift

import SwiftUI
import shared

@main
struct iOSApp: App {
    init() {
        // Initialize the shared Koin container
        KoinIOSKt.initKoin()
    }

    var body: some Scene {
        WindowGroup {
            ContentView()
        }
    }
}

5. Injecting Platform-Specific Dependencies

Often, you'll need to inject dependencies that have different implementations on Android and iOS (e.g., local storage directories, secure keychains, or Bluetooth managers). We can achieve this by combining Koin with Kotlin's expect/actual keywords.

For example, let's inject a platform-specific logger:

  1. Declare the interface and expect module in commonMain:
// commonMain/kotlin/com/example/portfolio/platform/PlatformLogger.kt
interface PlatformLogger {
    fun log(message: String)
}

// Expect Koin to receive a platform-specific module
expect val platformModule: org.koin.core.module.Module
  1. Provide the actual implementation on Android (androidMain):
// androidMain/kotlin/com/example/portfolio/platform/AndroidLogger.kt
import android.util.Log

class AndroidLogger : PlatformLogger {
    override fun log(message: String) {
        Log.d("KMP-App", message)
    }
}

actual val platformModule = org.koin.dsl.module {
    single<PlatformLogger> { AndroidLogger() }
}
  1. Provide the actual implementation on iOS (iosMain):
// iosMain/kotlin/com/example/portfolio/platform/IOSLogger.kt
import platform.Foundation.NSLog

class IOSLogger : PlatformLogger {
    override fun log(message: String) {
        NSLog("KMP-App: %s", message)
    }
}

actual val platformModule = org.koin.dsl.module {
    single<PlatformLogger> { IOSLogger() }
}
  1. Add platformModule to your commonModules list:
val commonModules = listOf(
    networkModule,
    databaseModule,
    repositoryModule,
    viewModelModule,
    platformModule // Platform logger injected transparently!
)

Conclusion

Using Koin in Kotlin Multiplatform bridges the DI gap between Android and iOS. By registering common business logic in standard modules and loading platform-specific actual instances dynamically, you maintain a highly clean, decoupled codebase. Testing becomes easy because you can swap out network clients or native loggers with mocks directly inside your common tests.

Now that your DI container is set up, read my guide on State Management in Kotlin Multiplatform to learn how to inject these view models into native UI flows, or explore Accessing Platform APIs: expect/actual vs. Interface Injection to master native-platform integration.


Need a Kotlin Multiplatform Expert?

Looking to establish a clean architecture with robust Dependency Injection (Koin), native persistence, or modular KMP design? I provide remote contract development and consulting services for engineering teams in Europe, the UK, and Canada with flexible timezone alignment.

Let's Connect & Discuss Your Project