Dependency injection frameworks do more than just wire objects together. They manage object lifetimes, enforce scoping rules, and make large apps testable. Understanding the internals of Hilt and Koin turns you from a user into someone who can debug DI problems and design injection hierarchies for large codebases.
Hilt Component Hierarchy
Hilt generates a set of components that form a hierarchy. Each component has a corresponding scope annotation.
SingletonComponent → @Singleton
ActivityRetainedComponent → @ActivityRetainedScoped
ActivityComponent → @ActivityScoped
FragmentComponent → @FragmentScoped
ViewModelComponent → @ViewModelScoped
ServiceComponent → @ServiceScoped
A @Singleton binding is created once per app process. A @ViewModelScoped binding is created once per ViewModel and destroyed when the ViewModel is cleared.
Providing Interfaces
You cannot annotate an interface with @Inject. Use a @Module to bind an implementation to an interface.
@Module
@InstallIn(SingletonComponent::class)
abstract class RepositoryModule {
@Binds
@Singleton
abstract fun bindUserRepository(
impl: UserRepositoryImpl
): UserRepository
}
@Binds is preferred over @Provides when you are binding an interface to a class Hilt can already construct. It generates less code.
Use @Provides when construction requires custom logic:
@Module
@InstallIn(SingletonComponent::class)
object NetworkModule {
@Provides
@Singleton
fun provideRetrofit(
okHttpClient: OkHttpClient
): Retrofit = Retrofit.Builder()
.baseUrl(BuildConfig.BASE_URL)
.client(okHttpClient)
.addConverterFactory(MoshiConverterFactory.create())
.build()
}
Qualifiers
When you need two different instances of the same type, use a qualifier.
@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class AuthInterceptorOkHttp
@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class LoggingInterceptorOkHttp
@Provides
@AuthInterceptorOkHttp
fun provideAuthClient(authInterceptor: AuthInterceptor): OkHttpClient =
OkHttpClient.Builder().addInterceptor(authInterceptor).build()
Custom Entry Points
To access Hilt bindings from code that Hilt does not directly inject (such as content providers or non-Hilt-managed classes), use @EntryPoint.
@EntryPoint
@InstallIn(SingletonComponent::class)
interface AnalyticsEntryPoint {
fun analyticsTracker(): AnalyticsTracker
}
val tracker = EntryPointAccessors.fromApplication(
context,
AnalyticsEntryPoint::class.java
).analyticsTracker()
Testing with Hilt
Replace production modules with test fakes using @TestInstallIn:
@TestInstallIn(
components = [SingletonComponent::class],
replaces = [RepositoryModule::class]
)
@Module
abstract class FakeRepositoryModule {
@Binds
@Singleton
abstract fun bindUserRepository(
fake: FakeUserRepository
): UserRepository
}
For individual test overrides, use @UninstallModules and provide a fresh module inside the test class.
Koin as an Alternative
Koin is a runtime DI framework built with Kotlin DSL. It has no code generation, making it simpler to understand but with runtime instead of compile-time safety.
val networkModule = module {
single { OkHttpClient.Builder().build() }
single { Retrofit.Builder().client(get()).build() }
}
val repositoryModule = module {
single<UserRepository> { UserRepositoryImpl(get()) }
}
val viewModelModule = module {
viewModel { ProfileViewModel(get()) }
}
Start Koin in Application.onCreate:
startKoin {
androidContext(this@App)
modules(networkModule, repositoryModule, viewModelModule)
}
Inject in a ViewModel or composable with by inject() or koinViewModel().
Hilt vs Koin
| Hilt | Koin | |
|---|---|---|
| Safety | Compile-time | Runtime |
| Performance | Better (generated code) | Slight overhead at startup |
| Boilerplate | More annotations | Less |
| Testing | @TestInstallIn, @UninstallModules | Module overrides |
| Best for | Large teams, strict boundaries | Smaller teams, fast iteration |
Both are valid. Hilt is the Google-recommended choice for most Android apps.
Practice
Create a @Singleton Retrofit module, a @ViewModelScoped use case, and write a test that replaces the repository module with a fake using @TestInstallIn. Verify the ViewModel receives the fake without changing production code.
Summary
Hilt's component hierarchy controls object lifetimes. Use @Binds for interfaces, @Provides for custom construction, qualifiers for multiple instances, and @TestInstallIn for clean test fakes. Koin trades compile-time safety for simplicity — choose based on team size and risk tolerance.