Koin для Kotlin: полное руководство с примерами

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Введение в Koin: что это и зачем нужно

Koin — это лёгкий, мощный и интуитивно понятный фреймворк для внедрения зависимостей (Dependency Injection, DI) в Kotlin. В отличие от тяжеловесных решений вроде Dagger или Spring, Koin не использует рефлексию, аннотации или генерацию кода. Всё строится на простых DSL-конструкциях (Domain Specific Language), что делает код чистым, читаемым и легко тестируемым.

Зачем нужен Koin? В современной разработке сложных приложений управление зависимостями между классами — одна из ключевых задач. Без DI вы будете создавать объекты вручную, что приводит к жёсткой связанности кода, усложняет тестирование и поддержку. Koin решает эти проблемы, предоставляя простой способ регистрировать, конфигурировать и получать зависимости.

Ключевые преимущества Koin:

  • Лёгкость: Минимальное влияние на размер APK и время сборки.
  • Простота: Не требует аннотаций, рефлексии или генерации кода.
  • Kotlin-first: Полностью использует возможности языка (DSL, лямбды, корутины).
  • Модульность: Позволяет разбивать зависимости на логические модули.
  • Тестируемость: Легко подменять зависимости в тестах.
  • Поддержка Android: Имеет специальные модули для Android (ViewModel, Fragment, Compose).

Установка Koin

Koin распространяется через Maven Central. Для подключения в проект добавьте зависимости в файл build.gradle.kts (или build.gradle).

Для Kotlin Multiplatform / JVM проекта:

// build.gradle.kts
dependencies {
    // Основной модуль Koin
    implementation("io.insert-koin:koin-core:3.5.6")
    
    // Для тестирования
    testImplementation("io.insert-koin:koin-test:3.5.6")
}

Для Android проекта:

// build.gradle.kts (app module)
dependencies {
    // Основной модуль
    implementation("io.insert-koin:koin-android:3.5.6")
    
    // Для ViewModel
    implementation("io.insert-koin:koin-androidx-viewmodel:3.5.6")
    
    // Для Jetpack Compose
    implementation("io.insert-koin:koin-androidx-compose:3.5.6")
    
    // Для WorkManager
    implementation("io.insert-koin:koin-androidx-workmanager:3.5.6")
    
    // Для тестирования
    testImplementation("io.insert-koin:koin-test:3.5.6")
    testImplementation("io.insert-koin:koin-test-junit5:3.5.6")
}

Важно: Версии могут меняться. Актуальную версию всегда можно проверить на GitHub репозитории Koin.

Быстрый старт с Koin

Рассмотрим простейший пример: у нас есть сервис UserService, который зависит от UserRepository. Вместо того чтобы создавать репозиторий вручную, мы используем Koin.

Шаг 1: Определяем классы

// Репозиторий пользователей
class UserRepository {
    fun getUsers(): List<String> = listOf("Alice", "Bob", "Charlie")
}

// Сервис, который использует репозиторий
class UserService(private val repository: UserRepository) {
    fun printUsers() {
        repository.getUsers().forEach { println(it) }
    }
}

Шаг 2: Создаём Koin модуль

// Модуль — это контейнер, где мы описываем, как создавать зависимости
val appModule = module {
    // single — создаст один экземпляр на всё приложение (синглтон)
    single<UserRepository> { UserRepository() }
    
    // factory — будет создавать новый экземпляр при каждом запросе
    factory { UserService(get()) } // get() — запрашивает зависимость UserRepository
}

Шаг 3: Запускаем Koin

// В точке входа приложения (main или Application)
fun main() {
    // Запускаем Koin с нашим модулем
    startKoin {
        modules(appModule)
    }
    
    // Получаем экземпляр UserService из контейнера
    val userService: UserService = getKoin().get()
    userService.printUsers()
    
    // Альтернативный способ — использовать inject()
    val userService2: UserService by inject()
    userService2.printUsers()
}

Результат: В консоль выведутся имена пользователей. При этом UserRepository будет создан автоматически и передан в UserService.

Основные методы и функции Koin

Рассмотрим ключевые методы Koin, которые вы будете использовать постоянно. Для каждого метода приведено описание, параметры и рабочий пример.

1. module { }

Что делает: Создаёт модуль Koin — логическую группу определений зависимостей. Модули можно комбинировать и подключать по необходимости.

Параметры: Принимает лямбда-выражение с DSL-определениями.

val networkModule = module {
    single { HttpClient() }
    single { ApiService(get()) }
}

val databaseModule = module {
    single { Database() }
    single { UserDao(get()) }
}

// Использование
startKoin {
    modules(networkModule, databaseModule)
}

2. single { }

Что делает: Регистрирует зависимость как синглтон. Экземпляр создаётся один раз при первом запросе и сохраняется в контейнере. Все последующие запросы возвращают тот же объект.

Параметры: Принимает лямбду, которая создаёт экземпляр. Можно указать тип через дженерик: single<MyInterface> { MyImpl() }.

class Database {
    val connection = "Connected to DB"
}

val appModule = module {
    single { Database() }
}

fun main() {
    startKoin { modules(appModule) }
    
    val db1: Database = getKoin().get()
    val db2: Database = getKoin().get()
    
    println(db1 === db2) // true — это один и тот же объект
}

3. factory { }

Что делает: Регистрирует зависимость, которая создаётся заново при каждом запросе. Полезно для объектов с состоянием или короткоживущих объектов (например, Presenter, ViewModel в некоторых архитектурах).

Параметры: Аналогично single — лямбда с созданием экземпляра.

class RequestHandler {
    val id = System.currentTimeMillis()
}

val appModule = module {
    factory { RequestHandler() }
}

fun main() {
    startKoin { modules(appModule) }
    
    val handler1: RequestHandler = getKoin().get()
    val handler2: RequestHandler = getKoin().get()
    
    println(handler1 === handler2) // false — разные объекты
    println("${handler1.id} != ${handler2.id}") // разные ID
}

4. get()

Что делает: Основной метод для получения зависимости из контейнера. Может использоваться как внутри модуля (для разрешения зависимостей), так и в коде приложения.

Параметры: Необязательный параметр — тип зависимости (обычно выводится автоматически). Можно передать имя (qualifier) или параметры.

// Внутри модуля — автоматическое разрешение
val appModule = module {
    single { Repository() }
    single { Service(get()) } // get() автоматически найдёт Repository
}

// В коде приложения
fun main() {
    startKoin { modules(appModule) }
    
    val service: Service = getKoin().get() // или просто get() внутри KoinContext
    val repository: Repository = getKoin().get()
    
    // С квалификатором (именем)
    val specialRepo: Repository = getKoin().get(named("special"))
}

5. by inject()

Что делает: Делегированное свойство (property delegate) для ленивого получения зависимости. Зависимость будет получена только при первом обращении к свойству.

Параметры: Аналогично get() — можно указать тип, квалификатор.

class MyActivity {
    // Зависимость будет получена при первом обращении к userService
    private val userService: UserService by inject()
    
    fun onStart() {
        userService.loadUsers() // здесь происходит реальное получение
    }
}

// В Android Activity/Fragment
class MainActivity : AppCompatActivity() {
    private val viewModel: MainViewModel by viewModel() // специальная функция для ViewModel
    private val repository: UserRepository by inject()
}

6. named()

Что делает: Создаёт квалификатор (имя) для зависимости. Позволяет различать несколько зависимостей одного типа.

Параметры: Принимает строку — имя квалификатора.

val appModule = module {
    single(named("local")) { LocalDataSource() }
    single(named("remote")) { RemoteDataSource() }
    single { Repository(get(named("local")), get(named("remote"))) }
}

// Получение
val localSource: DataSource = getKoin().get(named("local"))
val remoteSource: DataSource = getKoin().get(named("remote"))

7. scoped { }

Что делает: Создаёт зависимость, которая живёт в рамках определённого скоупа (области видимости). Экземпляр создаётся один раз для скоупа и уничтожается при его закрытии. Идеально для Android Activity, Fragment или пользовательских сессий.

Параметры: Принимает лямбду с созданием объекта. Скоуп должен быть объявлен внутри scope { } блока.

val appModule = module {
    scope(named("userSession")) {
        scoped { UserSession() }
        scoped { UserPreferences(get()) }
    }
}

// Использование
fun main() {
    startKoin { modules(appModule) }
    
    // Создаём скоуп
    val sessionScope = getKoin().createScope("session1", named("userSession"))
    
    val session: UserSession = sessionScope.get()
    val prefs: UserPreferences = sessionScope.get()
    
    // session и prefs будут жить, пока жив скоуп
    sessionScope.close() // уничтожаем скоуп и все его зависимости
}

8. startKoin { }

Что делает: Инициализирует Koin контейнер. Должен быть вызван один раз в точке входа приложения. Принимает конфигурацию через DSL.

Параметры: Блок конфигурации, где можно указать модули, свойства, логгер и т.д.

fun main() {
    startKoin {
        // Список модулей
        modules(networkModule, databaseModule, uiModule)
        
        // Свойства (можно загрузить из файла)
        properties(mapOf("api.url" to "https://api.example.com"))
        
        // Кастомный логгер
        logger(Level.INFO)
        
        // Загрузка свойств из файла
        fileProperties("/config.properties")
    }
}

9. getKoin()

Что делает: Возвращает глобальный экземпляр Koin контейнера. Позволяет получать зависимости, создавать скоупы и управлять контейнером.

Параметры: Нет.

fun main() {
    startKoin { modules(myModule) }
    
    val koin = getKoin()
    
    // Получить зависимость
    val service: MyService = koin.get()
    
    // Проверить, зарегистрирована ли зависимость
    if (koin.getOrNull<MyService>() != null) {
        println("MyService is registered")
    }
    
    // Получить все модули
    val allModules = koin.getAllModules()
}

10.

Рекомендаций