Впровадження залежностей в Android: Hilt vs Koin -- повний гайд та питання для співбесід 2026

Детальне порівняння Hilt 2.57 та Koin 4.2 для Android DI: практичні приклади коду, бенчмарки продуктивності, стратегії тестування та найпоширеніші питання на технічних співбесідах.

Hilt vs Koin Android Dependency Injection

Якість архітектури Android-додатку значною мірою визначається тим, як організовано управління залежностями між компонентами. Впровадження залежностей (Dependency Injection) дозволяє класам отримувати необхідні об'єкти ззовні, замість того щоб створювати їх самостійно, що забезпечує слабке зв'язування, тестованість та масштабованість коду. У 2026 році в екосистемі Android конкурують два основних фреймворки: Hilt версії 2.57.1, побудований на основі Dagger з валідацією графу залежностей на етапі компіляції, та Koin версії 4.2.1, легковагий service locator з Kotlin DSL та розв'язанням залежностей під час виконання. Правильний вибір між ними безпосередньо впливає на швидкість збірки, час запуску додатку, якість тестового покриття та ефективність онбордингу нових членів команди.

Compile-time проти Runtime DI

Hilt аналізує та валідує повний граф залежностей під час компіляції. Якщо якась прив'язка відсутня, збірка завершується помилкою ще до запуску додатку. Koin працює за іншим принципом: залежності реєструються через Kotlin DSL та розв'язуються під час виконання програми. Помилки конфігурації виявляються лише при зверненні до конкретної точки ін'єкції. Цей архітектурний контраст визначає всі подальші відмінності між двома фреймворками.

Принцип роботи Hilt та генерація коду під капотом

Hilt базується на Dagger та використовує генерацію коду для створення компонентів, що відповідають класам життєвого циклу Android. Анотація @HiltAndroidApp на класі Application ініціює процес генерації під час компіляції: KSP (Kotlin Symbol Processing) аналізує всі оголошені модулі та створює фабрики й провайдери для кожної прив'язки. Починаючи з версії 2.48, KSP замінив KAPT як рекомендований процесор анотацій, скоротивши час обробки приблизно на 50%.

Стандартна конфігурація Hilt складається з трьох елементів: модуля з оголошеннями прив'язок через анотації @Provides та @Binds, точки ін'єкції (Activity, Fragment або ViewModel) та класу Application з анотацією @HiltAndroidApp.

AppModule.ktkotlin
@Module
@InstallIn(SingletonComponent::class)
object AppModule {

    @Provides
    @Singleton
    fun provideRetrofit(): Retrofit {
        return Retrofit.Builder()
            .baseUrl("https://api.example.com/")
            .addConverterFactory(GsonConverterFactory.create())
            .build()
    }

    @Provides
    @Singleton
    fun provideUserRepository(
        retrofit: Retrofit
    ): UserRepository {
        return UserRepositoryImpl(retrofit.create(UserApi::class.java))
    }
}
UserViewModel.ktkotlin
@HiltViewModel
class UserViewModel @Inject constructor(
    private val userRepository: UserRepository
) : ViewModel() {

    private val _users = MutableStateFlow<List<User>>(emptyList())
    val users: StateFlow<List<User>> = _users.asStateFlow()

    fun loadUsers() {
        viewModelScope.launch {
            _users.value = userRepository.getUsers()
        }
    }
}

Компілятор гарантує, що UserRepository має дійсний провайдер ще до запуску додатку. Якщо прив'язка не знайдена, збірка завершиться помилкою компіляції, а не крашем у продакшені. Це особливо критично для великих проєктів, де ручне відстеження ланцюжків залежностей стає практично неможливим.

Налаштування Koin через Kotlin DSL

Koin обирає діаметрально протилежний підхід: нуль генерації коду, нуль обробки анотацій. Залежності описуються чистим Kotlin через DSL-конструкції single, factory та viewModel, а фреймворк розв'язує їх під час виконання програми через внутрішній реєстр сервісів. У версії 4.2 з'явились lazy-модулі для паралельного завантаження при старті додатку та оновлений рушій CoreResolverV2, що оптимізує роботу зі скоупами.

AppModule.ktkotlin
val appModule = module {
    single<Retrofit> {
        Retrofit.Builder()
            .baseUrl("https://api.example.com/")
            .addConverterFactory(GsonConverterFactory.create())
            .build()
    }

    single<UserRepository> {
        UserRepositoryImpl(get<Retrofit>().create(UserApi::class.java))
    }

    viewModel { UserViewModel(get()) }
}
MyApplication.ktkotlin
class MyApplication : Application() {
    override fun onCreate() {
        super.onCreate()
        startKoin {
            androidContext(this@MyApplication)
            modules(appModule)
        }
    }
}

DSL читається природно та інтуїтивно, не вимагаючи додаткових Gradle-плагінів для обробки анотацій. Водночас існує принциповий компроміс: друкарська помилка у виклику get() або відсутня реєстрація залежності проявиться лише під час виконання, коли відповідна точка ін'єкції буде активована.

Бенчмарки продуктивності: збірка проти запуску

Тема продуктивності DI-фреймворків з'являється практично на кожній технічній співбесіді з Android. Конкретні метрики мають значно більшу цінність, ніж абстрактні судження.

| Метрика | Hilt 2.57 (KSP) | Koin 4.2 | |--------|-----------------|----------| | Накладні витрати чистої збірки | +8-15s (генерація коду KSP) | ~0s (без генерації коду) | | Накладні витрати інкрементальної збірки | +2-4s | ~0s | | Запуск додатку (50 прив'язок) | ~0ms (попередньо згенерований код) | ~5-15ms (розв'язання графу) | | Запуск додатку (500 прив'язок) | ~0ms | ~30-80ms | | Вплив на розмір APK | +200-400 KB (згенерований код) | +100 KB (runtime бібліотека) | | Виявлення відсутніх прив'язок | Помилка компіляції | Crash у runtime |

Для проєктів з кількістю прив'язок до 100 runtime-витрати Koin практично непомітні. При перевищенні 300 прив'язок витрати на старт стають відчутними, хоча lazy-модулі у Koin 4.2 частково компенсують це завдяки паралельному завантаженню модулів.

KSP замість KAPT

Міграція з KAPT на KSP для обробки анотацій Hilt зменшує час обробки на 40-60%. Проєкти, що досі використовують KAPT разом із Hilt, мають якнайшвидше перейти на KSP. Hilt підтримує KSP починаючи з версії 2.48.

Стратегії тестування для Hilt та Koin

Тестованість -- це саме та сфера, де архітектурні відмінності набувають практичного значення. Hilt пропонує анотацію @TestInstallIn, яка замінює продакшн-модулі тестовими дублерами на етапі компіляції. Koin надає функцію loadKoinModules() для перевизначення прив'язок безпосередньо під час виконання тестів.

kotlin
// Hilt test module
@Module
@TestInstallIn(
    components = [SingletonComponent::class],
    replaces = [AppModule::class]
)
object FakeAppModule {

    @Provides
    @Singleton
    fun provideUserRepository(): UserRepository {
        return FakeUserRepository()
    }
}
kotlin
// Koin test
class UserViewModelTest : KoinTest {

    @Before
    fun setUp() {
        startKoin {
            modules(
                module {
                    single<UserRepository> { FakeUserRepository() }
                    viewModel { UserViewModel(get()) }
                }
            )
        }
    }

    @Test
    fun `loads users from repository`() {
        val viewModel: UserViewModel = get()
        viewModel.loadUsers()
        assertEquals(3, viewModel.users.value.size)
    }

    @After
    fun tearDown() {
        stopKoin()
    }
}

Тести з Hilt виявляють некоректну конфігурацію тестових модулів на етапі компіляції -- проєкт просто не збереться, якщо тестовий дублер не відповідає очікуваному інтерфейсу. Тести з Koin вимагають дисциплінованого управління життєвим циклом через startKoin/stopKoin, проте натомість надають більшу гнучкість для часткового перевизначення окремих залежностей без заміни цілого модуля.

Готовий до співбесід з Android?

Практикуйся з нашими інтерактивними симуляторами, flashcards та технічними тестами.

Багатомодульна архітектура: масштабування DI між фічами

Великі Android-проєкти розподіляють функціональність на окремі Gradle-модулі: :feature-payments, :feature-analytics, :feature-auth тощо. Фреймворк впровадження залежностей має коректно працювати з межами між цими модулями.

У Hilt кожен функціональний модуль оголошує власний @Module із зазначенням скоупу через @InstallIn. На етапі компіляції Hilt об'єднує всі модулі в єдину ієрархію компонентів. Архітектура MVVM органічно поєднується зі скоупованими компонентами Hilt, оскільки ViewModelComponent автоматично прив'язує залежності до життєвого циклу відповідної ViewModel.

feature-payments/PaymentModule.ktkotlin
@Module
@InstallIn(ViewModelComponent::class)
object PaymentModule {

    @Provides
    fun providePaymentGateway(
        retrofit: Retrofit
    ): PaymentGateway {
        return StripePaymentGateway(retrofit.create(PaymentApi::class.java))
    }
}

У Koin функціональні модулі експортують свої визначення через module {}, а головний модуль додатку завантажує їх усі при ініціалізації. Lazy-модулі Koin 4.2 дозволяють відкласти завантаження окремих функціональних модулів до моменту першого звернення, що зменшує час холодного старту.

feature-payments/paymentModule.ktkotlin
val paymentModule = module {
    factory<PaymentGateway> {
        StripePaymentGateway(get<Retrofit>().create(PaymentApi::class.java))
    }
}

// app/MyApplication.kt
startKoin {
    modules(appModule, paymentModule, analyticsModule)
}

Hilt забезпечує контроль видимості залежностей через скоупи компонентів на етапі компіляції. Koin покладається на конвенції команди: будь-який модуль технічно може звернутися до визначень будь-якого іншого модуля, що дає свободу, але створює ризик неявного зв'язування між фічами.

Kotlin Multiplatform та крос-платформний DI

Koin нативно підтримує Kotlin Multiplatform (KMP). Той самий DSL module {} працює однаково на Android, iOS, Desktop та Web. Hilt залишається виключно Android-рішенням, оскільки залежить від специфічних компонентів життєвого циклу Android та процесора анотацій Dagger, що не має аналогів на інших платформах.

Для проєктів, що мають працювати на кількох платформах, Koin є єдиним прийнятним варіантом з цих двох. Спільні модулі бізнес-логіки можуть оголосити залежності один раз у shared-коді та використовувати їх на всіх цільових платформах без дублювання конфігурації.

Типові питання на співбесідах з Android DI

Наведені питання регулярно з'являються на технічних співбесідах з Android. Відповіді побудовані лаконічно, з фокусом на те, що саме очікують почути інтерв'юери.

Питання: Яка різниця між compile-time та runtime dependency injection?

Compile-time DI (Hilt/Dagger) генерує код ін'єкції під час збірки. Компілятор перевіряє повний граф залежностей та виявляє відсутні прив'язки ще до запуску програми. Runtime DI (Koin) розв'язує залежності при першому зверненні через патерн service registry. Compile-time DI забезпечує швидший запуск за рахунок повільнішої збірки, тоді як runtime DI не впливає на час збірки, але відкладає виявлення помилок до моменту виконання.

Питання: Які скоупи компонентів існують у Hilt та як вони відповідають життєвим циклам Android?

SingletonComponent існує протягом усього часу роботи додатку. ActivityRetainedComponent переживає зміни конфігурації (наприклад, поворот екрана). ViewModelComponent прив'язаний до життєвого циклу конкретної ViewModel. ActivityComponent, FragmentComponent та ViewComponent слідують за відповідними власниками життєвого циклу Android. За необхідності ієрархію можна розширити власними скоупами.

Питання: Як Koin обробляє ін'єкцію ViewModel у Jetpack Compose?

Koin надає Composable-функцію koinViewModel(), яка створює або отримує ViewModel, прив'язану до найближчого ViewModelStoreOwner. Починаючи з Koin 4.2, функція koinNavViewModel() дозволяє прив'язувати ViewModel до конкретних записів навігаційного графу при використанні навігації Jetpack Compose.

Питання: У яких випадках Koin буде кращим вибором, ніж Hilt?

Koin доцільніше використовувати у KMP-проєктах (Hilt працює виключно на Android), у малих та середніх додатках, де час збірки важливіший за час запуску, та в командах, які віддають перевагу явному Kotlin DSL замість конфігурації на основі анотацій. Прототипування, MVP та proof-of-concept проєкти також виграють від мінімального налаштування Koin.

Поширена помилка на співбесіді

Називати Koin "фреймворком впровадження залежностей" технічно некоректно. Koin реалізує патерн service locator: залежності витягуються через виклик get(), а не передаються через конструктор. Інтерв'юери, обізнані з цією відмінністю, очікують від кандидатів чіткого розуміння цього нюансу. Hilt/Dagger виконує справжнє впровадження залежностей через згенеровані виклики конструкторів.

Фреймворк прийняття рішень: Hilt чи Koin

| Фактор | Hilt | Koin | |--------|------|------| | Валідація графу | Час компіляції | Runtime | | Вплив на час збірки | Вищий (KSP codegen) | Відсутній | | Вартість запуску | Близька до нуля | Зростає з розміром графу | | Крива навчання | Крутіша (концепції Dagger) | Плавна (чистий Kotlin DSL) | | Підтримка KMP | Ні | Так | | Інтеграція з Jetpack | Глибока (офіційна Google) | Добра (спільнота) | | Масштабованість команди | Сильніша (примусові скоупи) | Гнучка (на основі конвенцій) | | Тестування | @TestInstallIn (compile-safe) | loadKoinModules (runtime) |

Жоден із фреймворків не є універсально кращим. Оптимальний вибір залежить від масштабу проєкту, досвіду команди та цільових платформ.

Починай практикувати!

Перевір свої знання з нашими симуляторами співбесід та технічними тестами.

Висновок

  • Hilt 2.57 з KSP забезпечує повну валідацію графу залежностей на етапі компіляції та практично нульову вартість запуску, що робить його оптимальним рішенням для великих Android-проєктів з високими вимогами до надійності
  • Koin 4.2 з lazy-модулями та рушієм CoreResolverV2 суттєво скорочує відставання у продуктивності запуску, водночас зберігаючи нульовий вплив на час збірки та повну сумісність із Kotlin Multiplatform
  • Обидва фреймворки підтримують багатомодульні архітектури, проте Hilt контролює межі видимості залежностей на етапі компіляції, тоді як Koin покладається на дисципліну та конвенції команди
  • Для підготовки до Android-співбесід ключовим є розуміння фундаментального компромісу між compile-time та runtime DI, а також вміння аргументовано пояснити, коли доцільно обрати кожен із фреймворків
  • Міграція з одного фреймворку на інший є ресурсомісткою задачею, тому початковий вибір має враховувати довгострокову платформну стратегію, траєкторію зростання проєкту та кваліфікацію команди

Починай практикувати!

Перевір свої знання з нашими симуляторами співбесід та технічними тестами.

Теги

#android
#hilt
#koin
#dependency-injection
#kotlin

Поділитися

Пов'язані статті