Wstrzykiwanie zależności na Androidzie: Hilt vs Koin - kompletny przewodnik z pytaniami rekrutacyjnymi 2026

Szczegółowe porównanie Hilt 2.57 i Koin 4.2 dla Android DI: walidacja w czasie kompilacji vs runtime, benchmarki wydajności, testowanie, architektura wielomodułowa i pytania rekrutacyjne.

Hilt vs Koin Android Dependency Injection

Wstrzykiwanie zależności (Dependency Injection, DI) stanowi fundament dobrze zaprojektowanej aplikacji Android. Wzorzec ten determinuje sposób, w jaki klasy otrzymują swoje zależności, eliminując konieczność ich bezpośredniego tworzenia wewnątrz obiektów. W 2026 roku ekosystem Android oferuje dwa dominujące frameworki DI: Hilt w wersji 2.57.1 — oficjalne rozwiązanie Google zbudowane na bazie Daggera, realizujące walidację grafu zależności w czasie kompilacji — oraz Koin w wersji 4.2.1, lekki framework oparty na DSL Kotlina, rozwiązujący zależności w runtime. Wybór jednego z nich ma bezpośredni wpływ na czas budowania projektu, wydajność startu aplikacji, testowalność kodu i tempo onboardingu nowych członków zespołu.

Walidacja w czasie kompilacji vs runtime

Hilt weryfikuje cały graf zależności podczas kompilacji — brakujący binding powoduje błąd buildu, zanim aplikacja zostanie uruchomiona. Koin rozwiązuje zależności w runtime za pomocą DSL Kotlina, wykrywając błędy konfiguracji dopiero w momencie wykonania danej ścieżki kodu. Ta fundamentalna różnica determinuje wszystkie kompromisy między oboma frameworkami.

Jak działa Hilt pod maską

Hilt generuje komponenty Daggera mapowane na klasy cyklu życia Androida. Adnotacja @HiltAndroidApp uruchamia proces generowania kodu w czasie kompilacji, tworząc fabryki i providery dla każdego zadeklarowanego bindingu. Od wersji 2.48 zalecanym procesorem adnotacji jest KSP (Kotlin Symbol Processing), który zastąpił KAPT i skrócił czas przetwarzania adnotacji o blisko połowę.

Typowa konfiguracja Hilt składa się z trzech elementów: modułu deklarującego bindingi, punktu wejścia (Activity, Fragment lub ViewModel) oraz klasy Application oznaczonej adnotacją @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))
    }
}

Adnotacja @InstallIn(SingletonComponent::class) określa zakres życia bindingów — w powyższym przykładzie są one powiązane z cyklem życia całej aplikacji. Hilt definiuje predefiniowaną hierarchię komponentów: SingletonComponent, ActivityRetainedComponent, ViewModelComponent, ActivityComponent, FragmentComponent i ViewComponent, wymuszając poprawne zarządzanie zakresami na poziomie kompilacji.

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()
        }
    }
}

Kompilator weryfikuje, czy UserRepository posiada prawidłowego providera, zanim aplikacja zostanie uruchomiona. Brakujące bindingi ujawniają się jako błędy kompilacji, a nie awarie w trakcie działania aplikacji.

Konfiguracja Koin i podejście oparte na DSL Kotlina

Koin przyjmuje diametralnie odmienne podejście: zero generowania kodu, zero przetwarzania adnotacji. Zależności deklarowane są w czystym Kotlinie za pomocą DSL, a framework rozwiązuje je w runtime poprzez globalny rejestr usług. Wersja 4.2 wprowadza lazy modules umożliwiające równoległe ładowanie modułów podczas startu aplikacji oraz nowy silnik CoreResolverV2 optymalizujący rozwiązywanie zakresów.

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

    // Singleton repository bound to its interface
    single<UserRepository> {
        UserRepositoryImpl(get<Retrofit>().create(UserApi::class.java))
    }

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

DSL czyta się naturalnie i nie wymaga żadnej konfiguracji pluginu przetwarzania adnotacji w Gradle. Kompromisem jest fakt, że literówka w wywołaniu get() ujawni się dopiero w runtime, gdy dana ścieżka wstrzykiwania zostanie wykonana.

Benchmarki wydajności: czas budowania vs koszt runtime

Kwestia wydajności pojawia się praktycznie na każdej rozmowie rekrutacyjnej z Androida. Konkretne liczby mają większą wartość niż subiektywne opinie.

| Metryka | Hilt 2.57 (KSP) | Koin 4.2 | |--------|-----------------|----------| | Narzut czystego buildu | +8-15s (generowanie kodu KSP) | ~0s (brak generowania kodu) | | Narzut buildu przyrostowego | +2-4s | ~0s | | Start aplikacji (50 bindingów) | ~0ms (wygenerowany kod) | ~5-15ms (rozwiązywanie grafu) | | Start aplikacji (500 bindingów) | ~0ms | ~30-80ms | | Wpływ na rozmiar APK | +200-400 KB (wygenerowany kod) | +100 KB (biblioteka runtime) | | Wykrywanie brakujących bindingów | Błąd kompilacji | Crash w runtime |

W projektach z liczbą bindingów poniżej 100 narzut runtime Koin pozostaje niezauważalny dla użytkownika. Powyżej 300 bindingów koszt startu staje się mierzalny, choć mechanizm lazy modules w Koin 4.2 łagodzi ten problem przez równoległe ładowanie modułów.

KSP vs KAPT

Przejście z KAPT na KSP przy przetwarzaniu adnotacji Hilt skraca czas procesowania o 40-60%. Każdy projekt nadal wykorzystujący KAPT z Hilt powinien niezwłocznie przeprowadzić migrację do KSP. Hilt wspiera KSP od wersji 2.48.

Strategie testowania w obu frameworkach

Testowalność to obszar, w którym różnice architektoniczne przekładają się na praktykę codziennej pracy. Hilt oferuje adnotację @TestInstallIn do podmiany modułów produkcyjnych na testowe w czasie kompilacji. Koin udostępnia mechanizm loadKoinModules() do nadpisywania definicji w runtime.

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

    @Provides
    @Singleton
    fun provideUserRepository(): UserRepository {
        return FakeUserRepository() // In-memory test double
    }
}
kotlin
// Koin test — override at runtime
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()
    }
}

Testy Hilt wykrywają błędnie skonfigurowane moduły testowe na etapie kompilacji. Testy Koin wymagają starannego zarządzania cyklem życia kontenera (startKoin/stopKoin), ale oferują większą elastyczność przy częściowym nadpisywaniu modułów.

Gotowy na rozmowy o Android?

Ćwicz z naszymi interaktywnymi symulatorami, flashcards i testami technicznymi.

Architektura wielomodułowa: skalowanie DI między feature modules

Duże projekty Android dzielą funkcjonalności na oddzielne moduły Gradle. Framework DI musi poprawnie obsługiwać granice między modułami, zachowując izolację i czytelność grafu zależności.

W przypadku Hilt każdy feature module deklaruje własny moduł oznaczony adnotacją @Module i @InstallIn. Hilt scala wszystkie moduły w pojedynczą hierarchię komponentów w czasie kompilacji. Architektura MVVM naturalnie współgra ze scopowanymi komponentami Hilt.

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

    @Provides
    fun providePaymentGateway(
        retrofit: Retrofit // Provided by :app module
    ): PaymentGateway {
        return StripePaymentGateway(retrofit.create(PaymentApi::class.java))
    }
}

W Koin feature modules eksportują swoje deklaracje module {}, a moduł aplikacji ładuje je wszystkie podczas startu. Mechanizm lazy modules w Koin 4.2 pozwala na odroczone ładowanie feature modules do momentu pierwszego dostępu.

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

// app/MyApplication.kt — loads all feature modules
startKoin {
    modules(appModule, paymentModule, analyticsModule)
}

Hilt wymusza widoczność zależności poprzez zakresy komponentów — binding zainstalowany w ViewModelComponent nie może wstrzyknąć zależności o krótszym cyklu życia. Koin opiera się na konwencjach zespołu: dowolny moduł ma dostęp do definicji każdego innego modułu, co daje elastyczność, ale może prowadzić do niejawnych powiązań.

Kotlin Multiplatform a wybór frameworka DI

Koin wspiera Kotlin Multiplatform (KMP) natywnie. Ten sam DSL module {} działa na platformach Android, iOS, Desktop i Web. Hilt jest rozwiązaniem wyłącznie androidowym — opiera się na komponentach cyklu życia specyficznych dla Androida oraz na procesorze adnotacji Daggera.

Dla projektów celujących w wiele platform Koin pozostaje jedynym dostępnym wyborem spośród obu frameworków. Współdzielone moduły logiki biznesowej mogą deklarować swoje zależności jednokrotnie i wstrzykiwać je na wszystkich docelowych platformach.

Najczęstsze pytania rekrutacyjne: Hilt vs Koin

Poniższe pytania regularnie pojawiają się na rozmowach technicznych z Androida. Każda odpowiedź jest zwięzła i koncentruje się na tym, czego oczekuje osoba prowadząca rekrutację.

Pytanie: Czym różni się wstrzykiwanie zależności w czasie kompilacji od wstrzykiwania w runtime?

DI w czasie kompilacji (Hilt/Dagger) generuje kod wstrzykiwania podczas budowania projektu. Kompilator waliduje cały graf zależności, wychwytując brakujące bindingi przed uruchomieniem aplikacji. DI w runtime (Koin) rozwiązuje zależności w momencie ich pierwszego żądania, wykorzystując wzorzec rejestru usług. DI kompilacyjne zapewnia szybszy start aplikacji kosztem wolniejszych buildów; DI runtime eliminuje narzut na budowanie, ale odracza wykrywanie błędów.

Pytanie: Jakie zakresy komponentów definiuje Hilt i jak mapują się na cykl życia Androida?

SingletonComponent istnieje przez cały czas życia aplikacji. ActivityRetainedComponent przeżywa zmiany konfiguracji. ViewModelComponent jest powiązany z cyklem życia ViewModel. ActivityComponent, FragmentComponent i ViewComponent odpowiadają cyklom życia swoich właścicieli w hierarchii Android. Hierarchię tę można rozszerzać o niestandardowe zakresy.

Pytanie: Jak Koin obsługuje wstrzykiwanie ViewModel w Jetpack Compose?

Koin udostępnia koinViewModel() jako funkcję Composable, która tworzy lub pobiera ViewModel w zakresie najbliższego ViewModelStoreOwner. Od wersji 4.2 dostępna jest również funkcja koinNavViewModel(), która zakresuje ViewModele do wpisów grafu nawigacji przy korzystaniu z nawigacji Jetpack Compose.

Pytanie: Kiedy Koin będzie lepszym wyborem niż Hilt?

Koin sprawdza się lepiej w projektach KMP (Hilt działa wyłącznie na Androidzie), w małych i średnich aplikacjach, gdzie czas budowania ma większe znaczenie niż czas startu, oraz w zespołach preferujących jawny DSL Kotlina zamiast konfiguracji opartej na adnotacjach. Prototypowanie i projekty proof-of-concept również korzystają z minimalnej konfiguracji Koin.

Częsty błąd na rozmowach rekrutacyjnych

Określanie Koin jako "frameworka wstrzykiwania zależności" jest technicznie nieprecyzyjne. Koin jest lokatorem usług (Service Locator) — zależności są pobierane przez get(), a nie dostarczane przez wstrzykiwanie konstruktora. Rekruterzy znający to rozróżnienie oczekują, że kandydat je zasygnalizuje. Hilt/Dagger realizuje rzeczywiste wstrzykiwanie zależności poprzez generowane wywołania konstruktorów.

Ramka decyzyjna: wybór między Hilt a Koin

| Czynnik | Hilt | Koin | |--------|------|------| | Walidacja grafu | Czas kompilacji | Runtime | | Wpływ na czas buildu | Wyższy (generowanie kodu KSP) | Brak | | Koszt startu aplikacji | Bliski zeru | Rośnie proporcjonalnie do rozmiaru grafu | | Krzywa uczenia | Stroma (koncepcje Daggera) | Łagodna (czysty DSL Kotlina) | | Wsparcie KMP | Nie | Tak | | Integracja z Jetpack | Głęboka (oficjalna, Google) | Dobra (społeczność) | | Skalowalność w zespole | Silniejsza (wymuszone zakresy) | Elastyczna (oparta na konwencjach) | | Testowanie | @TestInstallIn (walidacja kompilacyjna) | loadKoinModules (runtime) |

Żaden z frameworków nie jest uniwersalnie lepszy. Wybór zależy od skali projektu, doświadczenia zespołu i docelowych platform.

Zacznij ćwiczyć!

Sprawdź swoją wiedzę z naszymi symulatorami rozmów i testami technicznymi.

Podsumowanie

  • Hilt 2.57 z KSP zapewnia walidację grafu w czasie kompilacji i niemal zerowy koszt startu aplikacji, co czyni go mocniejszym wyborem dla dużych, jednoplatformowych projektów Android o wysokich wymaganiach niezawodności
  • Koin 4.2 z mechanizmem lazy modules i silnikiem CoreResolverV2 zmniejsza dystans wydajnościowy przy starcie, zachowując zerowy wpływ na czas budowania i pełną kompatybilność z KMP
  • Oba frameworki obsługują architektury wielomodułowe, lecz Hilt wymusza granice zakresów na etapie kompilacji, podczas gdy Koin opiera się na konwencjach zespołu
  • W kontekście przygotowania do rozmów rekrutacyjnych z Androida zrozumienie kompromisu między walidacją kompilacyjną a runtime'ową oraz umiejętność uzasadnienia wyboru frameworka w konkretnym kontekście projektowym stanowią oczekiwane minimum
  • Migracja między frameworkami jest procesem nietrywalnym — początkowy wybór powinien uwzględniać długoterminową strategię platformową i kierunek skalowania projektu

Zacznij ćwiczyć!

Sprawdź swoją wiedzę z naszymi symulatorami rozmów i testami technicznymi.

Tagi

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

Udostępnij

Powiązane artykuły