Введение в 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.
Также в библиотеке
Очереди задач в Kotlin: Coroutines, Flow и библиотеки для фоновой обработки
Caffeine для Java: полное руководство с примерами
Интернационализация (i18n) в Java: полное руководство с примерами
Машинное обучение на PHP: обзор библиотек для ML
Работа с YAML в Swift (iOS/macOS) — библиотеки Yams и YamlSwift