Koin: внедрение зависимостей в KMP

Кто создаёт репозитории, API и базу — и как собрать это в один граф, общий для двух платформ.

Koin — мультиплатформенный DI-фреймворк, описывающий граф зависимостей в виде модулей (Kotlin DSL) и предоставляющий объекты по запросу без кодогенерации.

Зачем DI в общем модуле

Когда классов становится много (API, репозитории, база, settings, ViewModel), кто-то должен их создавать и связывать. Делать это руками в каждой точке — больно и не масштабируется. DI-контейнер описывает «как создать X» один раз, а дальше отдаёт готовые объекты. В KMP это особенно ценно: граф общего модуля один, а платформенные реализации (драйвер БД, движок сети) подставляются на своих местах.

Описание модуля

// commonMain
val sharedModule = module {
    single { HttpClient { install(ContentNegotiation) { json() } } }
    single { OrderApi(get()) }
    single<OrderRepository> { OrderRepositoryImpl(get(), get()) }
    factory { OrderViewModel(get()) }
}

single — один экземпляр на приложение, factory — новый при каждом запросе. get() просит контейнер подставить нужную зависимость.

Платформенные модули

Платформенные реализации описывают в платформенных модулях:

// androidMain
val androidModule = module {
    single<SqlDriver> { AndroidSqliteDriver(Schema, get(), "app.db") }
}

А запуск Koin происходит на каждой платформе со своим набором модулей: общий + платформенный.

Как работает под капотом

В отличие от Dagger/Hilt, которые генерируют код графа на компиляции, Koin строит граф в рантайме: модуль — это словарь «тип → как создать». Когда запрашивают OrderRepository, Koin находит его описание, рекурсивно создаёт зависимости через get() и возвращает объект. Плата — отсутствие проверки графа на этапе компиляции (ошибку «нет такой зависимости» увидите в рантайме). Зато Koin не требует кодогенерации, что упрощает его работу на Kotlin/Native, где не все генераторы доступны. Поэтому в KMP Koin — популярный выбор.

Частые ошибки

Забыть зарегистрировать платформенный модуль при старте — и получить рантайм-ошибку об отсутствии SqlDriver. Вторая — делать single там, где нужен factory (например, для ViewModel, привязанной к экрану), и держать устаревшее состояние. Третья — тащить в Koin-модуль платформенные классы внутри commonMain; платформенное регистрируется в платформенных модулях.

Итоги

  • Koin описывает граф зависимостей модулями на Kotlin DSL.
  • Граф строится в рантайме, без кодогенерации — удобно для Native.
  • Платформенные реализации регистрируются в платформенных модулях.
  • single для общих сервисов, factory — для одноразовых объектов.
Проверьте себя
1. Чем Koin отличается от Dagger/Hilt по принципу работы?
AKoin генерирует код на компиляции
BKoin строит граф в рантайме без кодогенерации
CKoin работает только на Android
DKoin не поддерживает single
2. Где регистрируют создание SqlDriver?
AВ общем модуле sharedModule
BВ платформенном модуле (androidModule/iosModule)
CВ Gradle
DНигде, он создаётся автоматически