Skip to main content
Version: 4.2

Kotlin Multiplatform Dependency Injection

Source Project

info

You can find the Kotlin Multiplatform project here: https://github.com/InsertKoinIO/hello-kmp

Platform Support

Koin Core supports all Kotlin Multiplatform targets:

PlatformTargetKoin ArtifactNotes
AndroidJVM/Androidkoin-androidFull Android integration (Activity, Fragment, ViewModel, etc.)
iOSNative (ARM64, X64, Simulator)koin-coreKoinComponent pattern for Swift interop
DesktopJVMkoin-coreWorks with Compose Desktop
WebJS, WASMkoin-coreExperimental WASM support
ServerJVM, Nativekoin-coreUse with Ktor, Spring, etc.
Linux/Windows/macOSNativekoin-coreNative targets supported

Gradle Setup

In your gradle/libs.versions.toml:

[versions]
koin = "4.2.0"

[libraries]
koin-core = { module = "io.insert-koin:koin-core", version.ref = "koin" }
koin-test = { module = "io.insert-koin:koin-test", version.ref = "koin" }
koin-android = { module = "io.insert-koin:koin-android", version.ref = "koin" }
koin-compose = { module = "io.insert-koin:koin-compose", version.ref = "koin" }

In your shared/build.gradle.kts:

kotlin {
// Your targets configuration
androidTarget()
iosX64()
iosArm64()
iosSimulatorArm64()
// Desktop, Web, etc.

sourceSets {
commonMain.dependencies {
implementation(libs.koin.core)
}

commonTest.dependencies {
implementation(libs.koin.test)
}

androidMain.dependencies {
implementation(libs.koin.android)
// For Compose on Android
implementation(libs.koin.compose)
}
}
}
info

For Compose Multiplatform integration, see Koin Compose.

Shared Koin Module

Platform specific components can be declared here, and be used later in Android or iOS (declared directly with actual classes or even actual module)

You can find the shared module sources here: https://github.com/InsertKoinIO/hello-kmp/tree/main/shared

// platform Module
val platformModule = module {
singleOf(::Platform)
}

// KMP Class Definition
expect class Platform() {
val name: String
}

// iOS
actual class Platform actual constructor() {
actual val name: String =
UIDevice.currentDevice.systemName() + " " + UIDevice.currentDevice.systemVersion
}

// Android
actual class Platform actual constructor() {
actual val name: String = "Android ${android.os.Build.VERSION.SDK_INT}"
}

Koin modules need to be gathered via a function:

// Common App Definitions
fun appModule() = listOf(commonModule, platformModule)

expect/actual Patterns with Koin

Kotlin Multiplatform provides two main patterns for platform-specific code: expect/actual and interfaces. Here's when to use each with Koin.

Pattern 1: expect/actual Classes (Platform-Specific Implementation)

Use when you need platform-specific APIs (Android Context, iOS UIDevice, etc.):

// commonMain - Declaration
expect class PlatformContext

expect fun createPlatformModule(): Module

// androidMain - Android Implementation
actual class PlatformContext(val context: Context)

actual fun createPlatformModule() = module {
single { PlatformContext(androidContext()) }
}

// iosMain - iOS Implementation
actual class PlatformContext

actual fun createPlatformModule() = module {
single { PlatformContext() }
}

Pattern 2: Interface + Platform Implementations (Dependency Injection)

Use when you want to inject different implementations per platform:

// commonMain - Interface & Module
interface Logger {
fun log(message: String)
}

val commonModule = module {
// Will be provided by platform modules
}

// androidMain - Android Implementation
class AndroidLogger : Logger {
override fun log(message: String) {
android.util.Log.d("App", message)
}
}

val androidModule = module {
single<Logger> { AndroidLogger() }
}

// iosMain - iOS Implementation
class IOSLogger : Logger {
override fun log(message: String) {
println("iOS: $message")
}
}

val iosModule = module {
single<Logger> { IOSLogger() }
}

Pattern 3: Hybrid - expect Module, actual Implementations

Best for complex platform modules:

// commonMain
expect val platformModule: Module

// androidMain
actual val platformModule = module {
single<Logger> { AndroidLogger() }
single { AndroidHttpClient() }
single { PlatformContext(androidContext()) }
}

// iosMain
actual val platformModule = module {
single<Logger> { IOSLogger() }
single { IOSHttpClient() }
single { PlatformContext() }
}
info

When to use which pattern:

  • expect/actual classes: Platform APIs (Context, UIDevice), simple platform differences
  • Interfaces: Business logic that varies by platform, testable code
  • expect modules: Complex platform-specific dependency graphs

Android Context in Shared Code

A common need is accessing Android Context in shared code. Here's the recommended pattern:

ContextWrapper Pattern

// commonMain - Wrapper interface
interface AppContext

// androidMain - Android implementation
class AndroidAppContext(val context: Context) : AppContext

val androidContextModule = module {
single<AppContext> { AndroidAppContext(androidContext()) }
}

// iosMain - Empty implementation
class IOSAppContext : AppContext

val iosContextModule = module {
single<AppContext> { IOSAppContext() }
}

Usage in shared code:

// commonMain - Repository can access platform context
class FileRepository(private val appContext: AppContext) {
fun saveFile(data: String) {
when (appContext) {
is AndroidAppContext -> {
// Android-specific file operations
val file = File(appContext.context.filesDir, "data.txt")
file.writeText(data)
}
is IOSAppContext -> {
// iOS-specific file operations (if needed)
}
}
}
}

val sharedModule = module {
single { FileRepository(get()) }
}
note

For pure shared logic, prefer abstracting platform operations into interfaces rather than using when statements. The above is for cases where platform-specific APIs are necessary.

Architecture Patterns in KMP

Repository Pattern

// commonMain - Domain layer
interface UserRepository {
suspend fun getUser(id: String): User
suspend fun saveUser(user: User)
}

val domainModule = module {
// Repository implementation provided by platform or data module
}

// commonMain - Data layer (can be in separate sourceSet)
class UserRepositoryImpl(
private val api: UserApi,
private val database: UserDatabase
) : UserRepository {
override suspend fun getUser(id: String): User {
return try {
api.fetchUser(id).also { database.saveUser(it) }
} catch (e: Exception) {
database.getUser(id)
}
}

override suspend fun saveUser(user: User) {
database.saveUser(user)
api.updateUser(user)
}
}

val dataModule = module {
single<UserRepository> { UserRepositoryImpl(get(), get()) }
}

Network Layer (Ktor + Koin)

// commonMain
val networkModule = module {
single {
HttpClient {
install(ContentNegotiation) {
json()
}
}
}

single { UserApi(get()) }
}

class UserApi(private val client: HttpClient) {
suspend fun fetchUser(id: String): User {
return client.get("https://api.example.com/users/$id").body()
}
}

Database Layer (Example with SqlDelight)

// commonMain
expect class DriverFactory {
fun createDriver(): SqlDriver
}

val databaseModule = module {
single { DriverFactory().createDriver() }
single { AppDatabase(get()) }
single { get<AppDatabase>().userQueries }
}

// androidMain
actual class DriverFactory(private val context: Context) {
actual fun createDriver(): SqlDriver {
return AndroidSqliteDriver(AppDatabase.Schema, context, "app.db")
}
}

// iosMain
actual class DriverFactory {
actual fun createDriver(): SqlDriver {
return NativeSqliteDriver(AppDatabase.Schema, "app.db")
}
}

ViewModel Pattern in KMP

// commonMain - Shared ViewModel (using koin-core-viewmodel or custom)
class UserViewModel(
private val repository: UserRepository
) : ViewModel() {
private val _user = MutableStateFlow<User?>(null)
val user: StateFlow<User?> = _user.asStateFlow()

fun loadUser(id: String) {
viewModelScope.launch {
_user.value = repository.getUser(id)
}
}
}

val viewModelModule = module {
// For Compose Multiplatform
viewModel { UserViewModel(get()) }

// Or for Android-specific
// viewModel { params -> UserViewModel(get(), params.get()) }
}

Module Organization Strategies

By Layer (Clean Architecture)

// commonMain
val dataModule = module {
single<UserRepository> { UserRepositoryImpl(get(), get()) }
single { UserApi(get()) }
single { UserDatabase(get()) }
}

val domainModule = module {
factory { GetUserUseCase(get()) }
factory { SaveUserUseCase(get()) }
}

val presentationModule = module {
viewModel { UserViewModel(get(), get()) }
}

// Combine all modules
fun appModules() = listOf(
networkModule,
databaseModule,
dataModule,
domainModule,
presentationModule,
platformModule
)

By Feature

// Feature 1: Authentication
val authModule = module {
single<AuthRepository> { AuthRepositoryImpl(get()) }
factory { LoginUseCase(get()) }
viewModel { LoginViewModel(get()) }
}

// Feature 2: Profile
val profileModule = module {
single<ProfileRepository> { ProfileRepositoryImpl(get()) }
factory { GetProfileUseCase(get()) }
viewModel { ProfileViewModel(get()) }
}

// Platform-specific features
expect val authPlatformModule: Module

fun appModules() = listOf(
authModule,
profileModule,
authPlatformModule
)

Testing KMP Modules

Unit Testing Shared Modules

// commonTest
class UserRepositoryTest : KoinTest {

@Test
fun testGetUser() = runTest {
startKoin {
modules(module {
single<UserApi> { FakeUserApi() }
single<UserDatabase> { FakeUserDatabase() }
single<UserRepository> { UserRepositoryImpl(get(), get()) }
})
}

val repository: UserRepository = get()
val user = repository.getUser("123")

assertEquals("John", user.name)

stopKoin()
}
}

Testing with Platform-Specific Dependencies

// commonTest
expect class TestPlatformContext

// androidTest
actual class TestPlatformContext(val context: Context)

// iosTest
actual class TestPlatformContext

// commonTest - Test module
class PlatformDependentTest : KoinTest {
@Test
fun testWithPlatformContext() {
startKoin {
modules(module {
single { TestPlatformContext() }
single { MyService(get()) }
})
}

val service: MyService = get()
// Test service that depends on platform context

stopKoin()
}
}

Common Pitfalls & Best Practices

✅ DO: Use interfaces for testable shared code

// Good - Testable
interface Logger {
fun log(message: String)
}

val sharedModule = module {
single { UserService(get<Logger>()) }
}

❌ DON'T: Use expect classes for business logic

// Bad - Hard to test, tight platform coupling
expect class Logger {
fun log(message: String)
}

✅ DO: Keep platform modules separate

// Good - Clear separation
fun initKoin() {
startKoin {
modules(commonModules() + platformModule)
}
}

❌ DON'T: Mix platform-specific code in shared modules

// Bad - Platform-specific code in commonMain
val sharedModule = module {
single {
if (Platform.isAndroid) { /* ... */ } // Don't do this!
}
}

✅ DO: Use lazy modules for large apps

// Good - Optimize startup
val lazyFeatureModule = lazyModule {
// Heavy dependencies loaded on-demand
}

startKoin {
modules(coreModules)
lazyModules(lazyFeatureModule)
}

❌ DON'T: Forget to close scopes

// Bad - Memory leak
class FeatureScreen : KoinComponent {
val scope = getKoin().createScope<FeatureScreen>()
// Forgot to close scope!
}

// Good - Proper cleanup
class FeatureScreen : KoinComponent {
val scope = getKoin().createScope<FeatureScreen>()

fun onDestroy() {
scope.close()
}
}

✅ DO: Use checkModules() in tests

@Test
fun checkKoinModules() {
koinApplication {
modules(allModules())
checkModules()
}
}

Platform-Specific Integration

Android App

You can keep using koin-android features and reuse the common modules/classes.

The code for the Android app can be found here: https://github.com/InsertKoinIO/hello-kmp/tree/main/androidApp

iOS App

The code for the iOS App can be found here: https://github.com/InsertKoinIO/hello-kmp/tree/main/iosApp

Calling Koin

Let’s prepare a wrapper to our Koin function (in our shared code):

// Helper.kt

fun initKoin(){
startKoin {
modules(appModule())
}
}

We can initialize it in our Main app entry:

@main
struct iOSApp: App {

// KMM - Koin Call
init() {
HelperKt.initKoin()
}

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

Injected Classes

Let’s call a Kotlin class instance from swift.

Our Kotlin component:

// Injection Boostrap Helper
class GreetingHelper : KoinComponent {
private val greeting : Greeting by inject()
fun greet() : String = greeting.greeting()
}

In our swift app:

struct ContentView: View {
// Create helper instance
let greet = GreetingHelper().greet()

var body: some View {
Text(greet)
}
}

Threading Considerations

On iOS and other Native targets, Koin instances are frozen by default with the new memory model. This generally works seamlessly, but be aware:

  • Koin definitions are thread-safe
  • Scopes can be created and used across threads
  • Use @SharedImmutable for global Koin instances if needed
note

The new Kotlin/Native memory model (enabled by default in Kotlin 1.7.20+) makes Koin usage much simpler. For older projects, see the Kotlin Native Memory Management documentation.

Desktop Platform Integration

For JVM Desktop apps (Compose Desktop):

// Desktop main
fun main() = application {
// Initialize Koin
startKoin {
modules(appModules())
}

Window(onCloseRequest = ::exitApplication) {
App()
}
}

Desktop-specific modules:

// desktopMain
val desktopModule = module {
single<Logger> { DesktopLogger() }
single { DesktopFileManager() }
}

Web Platform Integration (Experimental)

For Kotlin/JS and Kotlin/WASM:

// jsMain or wasmJsMain
fun main() {
startKoin {
modules(appModules())
}

// Your web app initialization
}

val webModule = module {
single<Logger> { ConsoleLogger() }
single { BrowserStorage() }
}
danger

WASM support is experimental. Some features may not work as expected.

Next Steps