Внедрение зависимостей (Dependency Injection) в Kotlin: полное руководство

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

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

Начать курс

Введение в 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.

Ми

Блоги

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