# Android Dependency Injection: Hilt vs Koin - Complete Guide and Interview Questions 2026 > Compare Hilt and Koin for Android dependency injection with practical code examples, performance benchmarks, and common interview questions. Updated for Hilt 2.57 and Koin 4.2. - Published: 2026-05-24 - Updated: 2026-05-24 - Author: SharpSkill - Tags: android, kotlin, hilt, koin, dependency-injection, dagger - Reading time: 9 min --- Android dependency injection determines how classes receive their dependencies instead of creating them directly. Two frameworks dominate the ecosystem in 2026: [Hilt](https://dagger.dev/hilt/) (Google's compile-time solution built on Dagger) at version 2.57.1, and [Koin](https://insert-koin.io/) (a lightweight runtime service locator) at version 4.2.1. Choosing the right one affects build times, startup performance, testability, and how quickly a team can onboard new developers. > **Compile-Time vs Runtime DI** > > Hilt validates the entire dependency graph during compilation and fails the build if any binding is missing. Koin resolves dependencies at runtime using a Kotlin DSL, catching configuration errors only when the affected code path executes. This fundamental difference drives every trade-off between the two frameworks. ## How Hilt Dependency Injection Works Under the Hood Hilt generates Dagger components mapped to Android lifecycle classes. The `@HiltAndroidApp` annotation triggers code generation at compile time, producing factories and providers for every declared binding. KSP (Kotlin Symbol Processing) replaced KAPT as the recommended annotation processor starting with Hilt 2.48, cutting annotation processing time roughly in half. A typical Hilt setup involves three pieces: a module declaring bindings, an entry point (Activity, Fragment, or ViewModel), and the Application class annotated with `@HiltAndroidApp`. ```kotlin // AppModule.kt @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)) } } ``` ```kotlin // UserViewModel.kt @HiltViewModel class UserViewModel @Inject constructor( private val userRepository: UserRepository ) : ViewModel() { private val _users = MutableStateFlow>(emptyList()) val users: StateFlow> = _users.asStateFlow() fun loadUsers() { viewModelScope.launch { _users.value = userRepository.getUsers() } } } ``` The compiler verifies that `UserRepository` has a valid provider before the app ever runs. Missing bindings surface as build errors, not runtime crashes. ## Koin Setup and the Kotlin DSL Approach Koin takes the opposite approach: zero code generation, zero annotation processing. Dependencies are declared in plain Kotlin using a DSL, and the framework resolves them at runtime through a global service registry. Koin 4.2 introduced lazy modules for parallel loading at startup and a new `CoreResolverV2` engine that optimizes scope resolution. ```kotlin // AppModule.kt val appModule = module { // Singleton Retrofit instance single { Retrofit.Builder() .baseUrl("https://api.example.com/") .addConverterFactory(GsonConverterFactory.create()) .build() } // Singleton repository bound to its interface single { UserRepositoryImpl(get().create(UserApi::class.java)) } // ViewModel with injected repository viewModel { UserViewModel(get()) } } ``` ```kotlin // MyApplication.kt class MyApplication : Application() { override fun onCreate() { super.onCreate() startKoin { androidContext(this@MyApplication) modules(appModule) } } } ``` The DSL reads naturally and requires no annotation processing plugin in the Gradle config. The trade-off: a typo in `get()` resolution only surfaces at runtime when that specific injection point executes. ## Performance Benchmarks: Build Time vs Runtime Cost The performance question comes up in almost every [Android interview](/technologies/android/interview-questions/android-dependency-injection). Concrete numbers matter more than opinions. | Metric | Hilt 2.57 (KSP) | Koin 4.2 | |--------|-----------------|----------| | Clean build overhead | +8-15s (KSP code generation) | ~0s (no code generation) | | Incremental build overhead | +2-4s | ~0s | | App startup (50 bindings) | ~0ms (pre-generated code) | ~5-15ms (graph resolution) | | App startup (500 bindings) | ~0ms | ~30-80ms | | APK size impact | +200-400 KB (generated code) | +100 KB (runtime library) | | Missing binding detection | Compile error | Runtime crash | For projects with fewer than 100 bindings, the runtime overhead of Koin stays imperceptible. Beyond 300 bindings, the startup cost becomes measurable, though Koin 4.2's lazy modules mitigate this by parallelizing module loading. > **KSP vs KAPT** > > Switching from KAPT to KSP for Hilt processing reduces annotation processing time by 40-60%. Any project still using KAPT with Hilt should migrate to KSP immediately. Hilt has supported KSP since version 2.48. ## Testing Strategies with Each Framework Testability is where the architectural differences become practical. Hilt provides `@TestInstallIn` to swap production modules with test doubles at compile time. Koin offers `loadKoinModules()` to override definitions at 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 { 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 tests catch misconfigured test modules at compile time. Koin tests require careful lifecycle management (`startKoin`/`stopKoin`) but offer more flexibility for partial module overrides. ## Multi-Module Architecture: Scaling DI Across Features Large Android projects split features into Gradle modules. The DI framework must support module boundaries cleanly. With Hilt, each feature module declares its own `@Module` annotated with `@InstallIn`. Hilt merges all modules into a single component hierarchy at compile time. The [MVVM architecture](/blog/android/mvvm-vs-mvi-architecture) pairs naturally with Hilt's scoped components. ```kotlin // feature-payments/PaymentModule.kt @Module @InstallIn(ViewModelComponent::class) object PaymentModule { @Provides fun providePaymentGateway( retrofit: Retrofit // Provided by :app module ): PaymentGateway { return StripePaymentGateway(retrofit.create(PaymentApi::class.java)) } } ``` With Koin, feature modules export their `module {}` declarations, and the app module loads them all at startup. Koin 4.2's lazy modules allow deferred loading of feature modules until first access. ```kotlin // feature-payments/paymentModule.kt val paymentModule = module { factory { StripePaymentGateway(get().create(PaymentApi::class.java)) } } // app/MyApplication.kt — loads all feature modules startKoin { modules(appModule, paymentModule, analyticsModule) } ``` Hilt enforces dependency visibility through component scopes. Koin relies on convention — any module can access any other module's definitions, which provides flexibility but can lead to implicit coupling. ## Kotlin Multiplatform Considerations Koin supports [Kotlin Multiplatform](https://kotlinlang.org/docs/multiplatform.html) (KMP) natively. The same `module {}` DSL works on Android, iOS, Desktop, and Web targets. Hilt is Android-only — it depends on Android-specific lifecycle components and the Dagger annotation processor. For projects targeting multiple platforms, Koin is the only viable option between the two. Shared business logic modules can declare their dependencies once and inject them across all targets. ## Common Interview Questions on Android DI These questions come up regularly in [Android technical interviews](/technologies/android). Each answer stays concise and focuses on what interviewers expect. **Q: What is the difference between compile-time and runtime dependency injection?** Compile-time DI (Hilt/Dagger) generates injection code during the build. The compiler validates the entire dependency graph, catching missing bindings before runtime. Runtime DI (Koin) resolves dependencies when they are first requested, using a service registry pattern. Compile-time DI produces faster startup but slower builds; runtime DI has zero build overhead but defers error detection. **Q: What Hilt component scopes exist and how do they map to Android lifecycles?** `SingletonComponent` lives for the entire application. `ActivityRetainedComponent` survives configuration changes. `ViewModelComponent` is scoped to a ViewModel's lifecycle. `ActivityComponent`, `FragmentComponent`, and `ViewComponent` follow their respective Android lifecycle owners. Custom scopes can extend this hierarchy. **Q: How does Koin handle ViewModel injection in Jetpack Compose?** Koin provides `koinViewModel()` as a Composable function that creates or retrieves a ViewModel scoped to the nearest `ViewModelStoreOwner`. Since Koin 4.2, `koinNavViewModel()` scopes ViewModels to navigation graph entries when using [Jetpack Compose navigation](/blog/android/jetpack-compose-interview-questions). **Q: When would Koin be a better choice than Hilt?** Koin fits better for KMP projects (Hilt is Android-only), small-to-medium apps where build time matters more than startup time, and teams that prefer explicit Kotlin DSL over annotation-driven configuration. Prototyping and proof-of-concept projects also benefit from Koin's minimal setup. > **Common Interview Mistake** > > Calling Koin a "dependency injection framework" is technically imprecise. Koin is a service locator — dependencies are pulled via `get()` rather than pushed via constructor injection. Interviewers familiar with the distinction will expect candidates to acknowledge this. Hilt/Dagger performs true dependency injection through generated constructor calls. ## Decision Framework: Choosing Between Hilt and Koin | Factor | Hilt | Koin | |--------|------|------| | Graph validation | Compile-time | Runtime | | Build time impact | Higher (KSP codegen) | None | | Startup cost | Near-zero | Scales with graph size | | Learning curve | Steeper (Dagger concepts) | Gentle (pure Kotlin DSL) | | KMP support | No | Yes | | Jetpack integration | Deep (official Google) | Good (community) | | Team scalability | Stronger (enforced scopes) | Flexible (convention-based) | | Testing | `@TestInstallIn` (compile-safe) | `loadKoinModules` (runtime) | Neither framework is universally superior. The choice depends on project size, team experience, and platform targets. ## Conclusion - Hilt 2.57 with KSP delivers compile-time graph validation and near-zero startup cost, making it the stronger choice for large, single-platform Android projects with strict reliability requirements - Koin 4.2 with lazy modules and `CoreResolverV2` closes the startup performance gap while maintaining zero build overhead and full KMP compatibility - Both frameworks handle multi-module architectures, but Hilt enforces scope boundaries at compile time while Koin relies on team conventions - For [Android interview preparation](/technologies/android), understanding the compile-time vs runtime trade-off and being able to articulate when each framework fits best is the expected baseline - Migrating between frameworks is non-trivial — the initial choice should account for the project's long-term platform and scaling trajectory --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/en/blog/android/android-dependency-injection-hilt-vs-koin