# Koin trong Kotlin Multiplatform: inject dependency gọn, không cần sinh code

> Viết module Koin 4.2 cho commonMain, khởi động trên Android và iOS, thay dependency khi test và bắt lỗi thiếu định nghĩa bằng verify(). Code đã build trên Kotlin 2.4.

- URL: https://dantech.academy/blog/kotlin/koin-kotlin-multiplatform-inject-dependency
- Ngày đăng: 2026-11-12
- Thời gian đọc: 5 phút
- Tags: kotlin, multiplatform, dependency-injection, koin

## Koin làm gì trong một app KMP

Dependency injection (DI) nghĩa là một class nhận những thứ nó cần qua constructor, thay vì tự tạo ra. `ScoreRepository` nhận một `ScoreStore`, `ScoreViewModel` nhận một `ScoreRepository`. Ai đó vẫn phải tạo các object này và nối chúng với nhau. Koin là thư viện làm phần đó, và nó chạy được trong `commonMain` của Kotlin Multiplatform (KMP).

Lúc viết bài, bản Koin mới nhất trên Maven Central là 4.2.2. Code trong bài mình build và test trên Koin 4.2.2 với Kotlin 2.4.10.

## Khai báo dependency

Thêm Koin vào `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")
        }
    }
}
```

`koin-compose-viewmodel` chỉ cần khi bạn lấy ViewModel trong Compose bằng `koinViewModel()`.

## Viết module

Một module Koin là danh sách "cần type này thì tạo thế nào". Ví dụ nhỏ, một repository điểm số và một 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)
    }
}
```

Vài thứ đáng để ý:

- `single` tạo một instance dùng chung cho cả app. `factory` (không có trong ví dụ) tạo instance mới mỗi lần được hỏi. `viewModel`/`viewModelOf` tạo ViewModel gắn với vòng đời của màn hình.
- `singleOf(::ScoreRepository)` đọc constructor và tự gọi `get()` cho từng tham số. Viết tay thì là `single { ScoreRepository(get()) }`. Cả hai đều là Kotlin thường, không cần annotation processor hay bước sinh code.
- `includes(platformModule())` gộp phần riêng của từng nền tảng vào. Mỗi nền tảng viết `actual fun platformModule()` trả về module của nó, ví dụ bản Android đọc `SharedPreferences`, bản iOS đọc `NSUserDefaults`. Trong ví dụ, cả hai đều dùng `InMemoryScoreStore` cho gọn:

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

## Khởi động Koin trên từng nền tảng

Gọi `initKoin()` một lần, ở chỗ app khởi động. Trên Android là `Application.onCreate()`:

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

Trên iOS, bạn gọi hàm đó từ Swift khi app khởi động. Hàm top-level của Kotlin hiện ra trong Swift dưới một class mang tên file, nên nếu `initKoin` nằm trong `Koin.kt` thì Swift gọi `KoinKt.doInitKoin()`. Tiền tố `do` là do Kotlin/Native tự thêm: Objective-C dành tên bắt đầu bằng `init` cho initializer.

Trong Compose, lấy ViewModel bằng `koinViewModel()`:

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

## Thay dependency khi test

Vì `ScoreRepository` chỉ biết interface `ScoreStore`, test có thể đưa vào một bản giả. Với Koin, bạn nạp module thật rồi ghi đè đúng một định nghĩa:

```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())
    }
}
```

`stopKoin()` sau mỗi test là bắt buộc. `startKoin` dùng một instance toàn cục, gọi lần thứ hai khi lần đầu chưa dừng sẽ báo lỗi.

## Bắt lỗi thiếu định nghĩa trước khi chạy app

Với DSL như trên, nếu bạn quên khai báo `ScoreStore`, app vẫn build được và chỉ crash khi Koin cần tạo `ScoreRepository`. Koin có `verify()` trong `koin-test` để bắt lỗi này bằng một unit test. Theo tài liệu Koin, `verify()` chỉ chạy trên JVM, nên trong KMP bạn đặt test này ở source set host test của Android:

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

Mình thử xóa dòng `single<ScoreStore>` khỏi module Android, test này fail với `MissingKoinDefinitionException`. Trên Koin 4.2.2, `verify()` vẫn được đánh dấu `@KoinExperimentalAPI`, nên cần dòng `@OptIn` ở trên.

Koin 4.2 còn có một Compiler Plugin kiểm tra toàn bộ đồ thị dependency lúc build, và tài liệu Koin khuyên dùng nó cho project mới trên Kotlin 2.3.20 trở lên. Plugin này là tùy chọn. DSL ở trên chạy được mà không cần nó, và bạn có thể thêm plugin sau mà không phải viết lại module. Nếu bạn từng dùng annotation của Koin qua KSP: bộ xử lý KSP (`koin-ksp-compiler`) đã bị deprecated, còn bản thân các annotation thì không.

## Học tiếp

Khóa [Kotlin Mobile 101](https://dantech.academy/courses/kotlin-mobile-101) dạy inject dependency bằng Koin trên một app Kotlin Multiplatform hoàn chỉnh, kèm unit test chạy trong vài giây.
