Введение в Dependency Injection в Kotlin
Современная разработка на Kotlin требует грамотного управления зависимостями между классами. Dependency Injection (DI) — это паттерн проектирования, который позволяет передавать объекту его зависимости извне, а не создавать их внутри. Это снижает связанность кода, упрощает тестирование и делает приложение более гибким.
В этой статье мы разберём, что такое внедрение зависимостей, зачем оно нужно, и рассмотрим три подхода к его реализации в Kotlin: ручное внедрение, использование библиотеки Hilt и лёгкого фреймворка Koin. Материал будет полезен как новичкам, так и опытным разработчикам, желающим упорядочить архитектуру своих проектов.
Основы Dependency Injection
Проблема без DI
Представьте класс UserService, который напрямую создаёт экземпляр UserRepository:
class UserService {
private val repository = UserRepository() // жёсткая связь
fun getUser(id: Int): User {
return repository.findById(id)
}
}
Такой код трудно тестировать: вы не сможете подменить UserRepository на mock-объект. Кроме того, при изменении конструктора UserRepository придётся править все места его создания.
Решение: внедрение через конструктор
Самый простой способ применить DI — передать зависимость через конструктор:
class UserService(private val repository: UserRepository) {
fun getUser(id: Int): User {
return repository.findById(id)
}
}
Теперь UserService не знает, как создаётся UserRepository. Вы можете передать любую реализацию — настоящую базу данных, фейковый слой или mock для тестов.
Инверсия управления (IoC)
DI тесно связан с принципом Inversion of Control (IoC). Вместо того чтобы класс сам контролировал создание зависимостей, управление передаётся внешнему контейнеру или фабрике. Это делает систему слабосвязанной и легко расширяемой.
Ручное внедрение зависимостей (Manual DI)
Для небольших проектов или учебных целей можно обойтись без фреймворков. Создадим простой контейнер зависимостей вручную.
Пример: контейнер зависимостей
// Определяем интерфейс репозитория
interface UserRepository {
fun findById(id: Int): User
}
// Реализация
class UserRepositoryImpl : UserRepository {
override fun findById(id: Int): User {
// логика получения пользователя из БД
return User(id, "User$id")
}
}
// Контейнер, который собирает все зависимости
class AppContainer {
val userRepository: UserRepository = UserRepositoryImpl()
val userService: UserService by lazy { UserService(userRepository) }
}
// Использование
fun main() {
val container = AppContainer()
val service = container.userService
println(service.getUser(1))
}
Плюсы: полный контроль, нет внешних зависимостей, простота понимания.
Минусы: при росте проекта контейнер разрастается, становится сложно управлять жизненным циклом объектов и их скоупами.
Внедрение зависимостей с Hilt
Hilt — это официальная библиотека от Google для DI в Android-приложениях на Kotlin. Она построена на основе Dagger, но значительно проще в настройке.
Настройка Hilt
Добавьте в build.gradle.kts (уровень проекта):
plugins {
id("com.google.dagger.hilt.android") version "2.48" apply false
}
В build.gradle.kts модуля приложения:
plugins {
id("com.google.dagger.hilt.android")
}
dependencies {
implementation("com.google.dagger:hilt-android:2.48")
kapt("com.google.dagger:hilt-compiler:2.48")
}
Пример использования
// Модуль, который предоставляет зависимость
@Module
@InstallIn(SingletonComponent::class)
object AppModule {
@Provides
@Singleton
fun provideUserRepository(): UserRepository {
return UserRepositoryImpl()
}
}
// Класс, в который внедряется зависимость
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
@Inject
lateinit var userService: UserService
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// userService уже проинициализирован
}
}
Hilt автоматически управляет жизненным циклом объектов. Вы можете указать скоуп (@Singleton, @ActivityScoped и т.д.) — и библиотека сама создаст и уничтожит экземпляр в нужный момент.
Преимущества Hilt: высокая производительность (компиляция в код), поддержка Jetpack, строгая типизация.
Недостатки: долгая компиляция, необходимость изучения Dagger-аннотаций.
Внедрение зависимостей с Koin
Koin — это лёгкий DI-фреймворк для Kotlin, который работает без генерации кода. Он идеально подходит для мультиплатформенных проектов и приложений, где важен быстрый старт.
Настройка Koin
Добавьте в build.gradle.kts:
dependencies {
implementation("io.insert-koin:koin-core:3.5.0")
// Для Android
implementation("io.insert-koin:koin-android:3.5.0")
}
Пример использования
// Модуль Koin
val appModule = module {
single<UserRepository> { UserRepositoryImpl() }
factory { UserService(get()) } // каждый вызов — новый экземпляр
}
// Инициализация в Application
class MyApp : Application() {
override fun onCreate() {
super.onCreate()
startKoin {
androidContext(this@MyApp)
modules(appModule)
}
}
}
// Внедрение в Activity
class MainActivity : AppCompatActivity() {
private val userService: UserService by inject()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// userService готов к использованию
}
}
Koin использует single для синглтонов и factory для создания новых экземпляров при каждом запросе. Всё настраивается через DSL Kotlin, без аннотаций.
Плюсы Koin: простота, быстрое подключение, мультиплатформенность, не требует kapt.
Ми