# Koin in Kotlin Multiplatform: lightweight dependency injection without code generation

> Write a Koin 4.2 module for commonMain, start it on Android and iOS, swap dependencies in tests and catch missing definitions with verify(). Code built on Kotlin 2.4.

- URL: https://dantech.academy/en/blog/kotlin/koin-kotlin-multiplatform-inject-dependency
- Published: 2026-11-12
- Reading time: 5 min
- Tags: kotlin, multiplatform, dependency-injection, koin

## What Koin does in a KMP app

Dependency injection (DI) means a class receives what it needs through its constructor instead of creating it. `ScoreRepository` receives a `ScoreStore`, and `ScoreViewModel` receives a `ScoreRepository`. Something still has to create those objects and wire them together. Koin is a library that does that part, and it runs in the `commonMain` of a Kotlin Multiplatform (KMP) project.

At the time of writing, the latest Koin release on Maven Central is 4.2.2. I built and tested the code in this post on Koin 4.2.2 with Kotlin 2.4.10.

## Add the dependencies

Add Koin to `commonMain`:

```kotlin
// shared/build.gradle.kts
kotlin {
    sourceSets {
        commonMain.dependencies {
            implementation("io.insert-koin:koin-core:4.2.2")
            implementation("io.insert-koin:koin-compose-viewmodel:4.2.2")
        }
    }
}
```

You only need `koin-compose-viewmodel` if you get ViewModels in Compose with `koinViewModel()`.

## Write a module

A Koin module is a list of "when someone needs this type, build it like this". A small example with a score repository and a ViewModel:

```kotlin
// commonMain
package com.example.koin

import androidx.lifecycle.ViewModel
import org.koin.core.context.startKoin
import org.koin.core.module.Module
import org.koin.core.module.dsl.singleOf
import org.koin.core.module.dsl.viewModelOf
import org.koin.dsl.KoinAppDeclaration
import org.koin.dsl.module

interface ScoreStore {
    fun save(score: Int)
    fun best(): Int
}

class InMemoryScoreStore : ScoreStore {
    private var bestScore = 0

    override fun save(score: Int) {
        if (score > bestScore) bestScore = score
    }

    override fun best(): Int = bestScore
}

class ScoreRepository(private val store: ScoreStore) {
    fun submit(score: Int) = store.save(score)
    fun best(): Int = store.best()
}

class ScoreViewModel(private val repository: ScoreRepository) : ViewModel() {
    fun best(): Int = repository.best()
}

expect fun platformModule(): Module

val appModule = module {
    includes(platformModule())
    singleOf(::ScoreRepository)
    viewModelOf(::ScoreViewModel)
}

fun initKoin(config: KoinAppDeclaration? = null) {
    startKoin {
        config?.invoke(this)
        modules(appModule)
    }
}
```

A few things worth noticing:

- `single` creates one instance shared by the whole app. `factory` (not used here) creates a new instance every time it is requested. `viewModel`/`viewModelOf` creates a ViewModel tied to a screen's lifecycle.
- `singleOf(::ScoreRepository)` reads the constructor and calls `get()` for each parameter. Written by hand it would be `single { ScoreRepository(get()) }`. Both are plain Kotlin, with no annotation processor and no code generation step.
- `includes(platformModule())` pulls in each platform's own part. Every platform writes `actual fun platformModule()` returning its module, for example reading `SharedPreferences` on Android and `NSUserDefaults` on iOS. To keep the example short, both use `InMemoryScoreStore`:

```kotlin
// androidMain and iosMain
actual fun platformModule(): Module = module {
    single<ScoreStore> { InMemoryScoreStore() }
}
```

## Start Koin on each platform

Call `initKoin()` once, where the app starts. On Android that is `Application.onCreate()`:

```kotlin
class ScoreApplication : Application() {
    override fun onCreate() {
        super.onCreate()
        initKoin()
    }
}
```

On iOS, you call it from Swift at app launch. Kotlin top-level functions show up in Swift under a class named after the file, so if `initKoin` lives in `Koin.kt`, Swift calls `KoinKt.doInitKoin()`. Kotlin/Native adds the `do` prefix because Objective-C reserves names starting with `init` for initializers.

In Compose, get the ViewModel with `koinViewModel()`:

```kotlin
@Composable
fun ScoreScreen(viewModel: ScoreViewModel = koinViewModel()) {
    Text("Best: ${viewModel.best()}")
}
```

## Swap a dependency in tests

Because `ScoreRepository` only knows the `ScoreStore` interface, a test can hand it a fake. With Koin, you load the real module and override exactly one definition:

```kotlin
// commonTest
class FakeScoreStore(private val fixedBest: Int) : ScoreStore {
    override fun save(score: Int) = Unit
    override fun best(): Int = fixedBest
}

class ScoreRepositoryKoinTest {
    @AfterTest
    fun tearDown() {
        stopKoin()
    }

    @Test
    fun repositoryReadsTheStoreKoinGaveIt() {
        val koin = startKoin {
            allowOverride(true)
            modules(appModule, module { single<ScoreStore> { FakeScoreStore(fixedBest = 42) } })
        }.koin

        assertEquals(42, koin.get<ScoreRepository>().best())
    }
}
```

Calling `stopKoin()` after each test is required. `startKoin` uses one global instance, and starting it a second time without stopping the first throws an error.

## Catch missing definitions before running the app

With the DSL above, if you forget to declare `ScoreStore`, the app still builds and only crashes when Koin has to create `ScoreRepository`. Koin's `verify()` in `koin-test` catches this in a unit test. According to the Koin docs, `verify()` runs on the JVM only, so in KMP you put this test in the Android host test source set:

```kotlin
// androidHostTest, needs io.insert-koin:koin-test
@OptIn(KoinExperimentalAPI::class)
class AppModuleCheck {
    @Test
    fun appModuleIsComplete() {
        appModule.verify()
    }
}
```

I removed the `single<ScoreStore>` line from the Android module, and this test failed with `MissingKoinDefinitionException`. On Koin 4.2.2, `verify()` is still marked `@KoinExperimentalAPI`, which is why the `@OptIn` line is there.

Koin 4.2 also has a Compiler Plugin that checks the whole dependency graph at build time, and the Koin docs recommend it for new projects on Kotlin 2.3.20 or later. The plugin is optional. The DSL above works without it, and you can add the plugin later without rewriting your modules. If you have used Koin annotations through KSP: the KSP processor (`koin-ksp-compiler`) is deprecated, but the annotations themselves are not.

## Keep learning

[Kotlin Mobile 101](https://dantech.academy/courses/kotlin-mobile-101) teaches dependency injection with Koin on a complete Kotlin Multiplatform app, with unit tests that run in a few seconds.
