Comprehensive Guide to Using Hilt for Dependency Injection in Android
This article explains the fundamentals of dependency injection, introduces Hilt and its basic setup for Android, demonstrates advanced features such as qualifiers, scopes, assisted injection, custom entry points, and analyzes Hilt's underlying principles, helping developers decide whether to adopt DI frameworks in Android projects.
When working on a project where code is difficult to understand, you may discover that Hilt is being used for dependency injection. Although Hilt simplifies Dagger, it still has a learning curve for beginners, and many online articles merely copy official documentation, making deep understanding challenging. This guide aims to help you master Hilt.
What is Dependency Injection? Using a phone as an analogy, a phone depends on software and hardware. In code, this translates to classes that depend on other objects, such as class FishPhone { val software = Software(); val hardware = Hardware(); fun call() { software.handle(); hardware.handle() } } . Dependency injection (DI) moves the creation of these dependent objects outside the class, allowing them to be supplied externally.
Why use a DI framework? Without DI, each consumer must manually instantiate dependencies, leading to tightly coupled code and boilerplate. A DI framework manages object creation, injection, and lifecycle, allowing developers to declare required types without worrying about their construction.
Introducing Hilt Hilt is the Android‑specific wrapper around Dagger, offering a simpler setup. To add Hilt, include the plugin in the project‑level build.gradle and the library in the module‑level build.gradle : plugins { id 'com.google.dagger.hilt.android' version '2.48.1' apply false } plugins { id 'com.android.application' id 'org.jetbrains.kotlin.android' id 'com.google.dagger.hilt.android' id 'kotlin-kapt' } implementation 'com.google.dagger:hilt-android:2.48.1' kapt 'com.google.dagger:hilt-compiler:2.48.1'
Mark the application class with @HiltAndroidApp to enable the DI environment, then inject objects using @Inject : @HiltAndroidApp class MyApp : Application() { @Inject lateinit var software: Software } The Software class must also be annotated with @Inject constructor to be injectable.
Injecting interfaces Directly injecting an interface fails because Hilt does not know the implementation. Define a module with @Binds to bind the implementation: @Module @InstallIn(SingletonComponent::class) abstract class SoftwareModule { @Binds abstract fun bindSoftware(impl: SoftwareImpl): ISoftware } Now @Inject lateinit var software: ISoftware works.
Injecting third‑party classes When you cannot modify a class, provide it via a @Provides method in a module: @Module @InstallIn(SingletonComponent::class) object HardwareModule { @Provides fun provideHardware(): Hardware = Hardware() } This also works for interfaces by returning the concrete implementation.
Qualifiers When multiple implementations exist, use custom qualifier annotations to disambiguate: @Qualifier @Retention(AnnotationRetention.BINARY) annotation class US @Qualifier @Retention(AnnotationRetention.BINARY) annotation class China @Module @InstallIn(SingletonComponent::class) abstract class SoftwareModule { @Binds @US abstract fun bindSoftwareUs(impl: SoftwareUS): ISoftware @Binds @China abstract fun bindSoftwareChina(impl: SoftwareChina): ISoftware } Inject with @Inject @US lateinit var softwareUs: ISoftware and @Inject @China lateinit var softwareChina: ISoftware .
Predefined qualifiers Hilt already provides @ApplicationContext and @ActivityContext , eliminating the need to create your own context qualifiers.
Component scopes and lifecycles Hilt generates components that align with Android lifecycles (SingletonComponent, ActivityComponent, ViewModelComponent, etc.). Use scope annotations like @Singleton , @ActivityScoped , @ViewModelScoped to control instance lifetimes. The scope must match the component it is installed in.
Assisted injection For constructor parameters that cannot be provided by Hilt (e.g., runtime strings), use @AssistedInject and an @AssistedFactory : @AssistedInject class Hardware @Inject constructor(@ApplicationContext val context: Context, @Assisted val version: String) { @AssistedFactory interface Factory { fun create(version: String): Hardware } } Inject the factory and create instances with the required runtime arguments.
Custom entry points When a class is not an Android injection target, define an entry point interface: @InstallIn(SingletonComponent::class) interface HardwarePoint { fun getHardware(): Hardware } Retrieve it via EntryPointAccessors.fromApplication(context, HardwarePoint::class.java) .
Injecting object classes Provide a singleton instance of an object via a @Provides method: @Provides @Singleton fun provideSystem(): MySystem = MySystem.getSelf() Then inject MySystem normally.
Hilt internals Hilt generates a subclass (e.g., Hilt_SecondActivity ) that registers an OnContextAvailableListener . When the context becomes available, Hilt calls the generated injector to perform field injection, ensuring injected fields are ready after onCreate() .
Should Android use DI? While DI adds complexity, it greatly simplifies large MVVM/MVI architectures by decoupling layers such as ViewModel, Repository, DataSources, and network clients. The article concludes that modern Android projects benefit from adopting Hilt for clean, maintainable code.
Rare Earth Juejin Tech Community
Juejin, a tech community that helps developers grow.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.