Tag: gradle

  • How to Use Hilt Dependency Injection in Android — Complete Setup Guide

    Dependency injection is one of those patterns that pays dividends the moment a project grows beyond a handful of classes. Instead of manually wiring dependencies together — and updating every call site when a constructor changes — you declare what a class needs, and the framework handles the rest. In the Android ecosystem, Hilt has become the standard way to do this, offering compile-time correctness, minimal runtime overhead, and deep integration with Jetpack lifecycle classes. Built directly on top of Dagger, Hilt inherits Dagger's proven bytecode-generation model while stripping away the configuration boilerplate that made Dagger notoriously difficult to set up in Android projects. Whether you are starting a new project or migrating an existing one, understanding Hilt's core concepts — entry points, modules, component scopes, and its suite of Jetpack integrations — gives you a solid foundation for writing maintainable, testable Android code. This guide walks through every step of that setup, from adding your first Gradle dependency to integrating Hilt with ViewModel, WorkManager, Navigation, and Compose.

    What Is Hilt and Why Use It?

    Hilt is a dependency injection library developed by Google and built on top of Dagger. The Android team designed it specifically to solve three problems that made Dagger cumbersome in mobile applications: excessive boilerplate during project setup, difficult-to-manage component scoping tied to Android lifecycle classes, and the absence of standard entry points for framework types like Activity and Fragment. Because Hilt inherits from Dagger, it brings Dagger's compile-time correctness — your build fails fast if a dependency cannot be resolved, not at runtime — along with Dagger's predictable runtime performance and scalability. Hilt generates the Dagger components and all injection code at compile time via bytecode transformation, meaning there is no reflection cost during app startup. The framework also auto-generates test components so your unit and integration tests get the same DI graph without additional configuration. In short, Hilt gives you everything Dagger offers — correctness, performance, and scalability — without the setup overhead that historically made Dagger a barrier to entry for Android developers.

    Project Setup: Gradle Dependencies and Application Class

    Getting Hilt into a new or existing Android project requires two changes to your Gradle files and one change to your Application class. The example below uses version 2.48, which is a recent stable release; always check Dagger's official release page for the latest version before starting.

    Project-level build.gradle

    Apply the Hilt Android Gradle plugin in the plugins block at the project level:
    plugins {
        id("com.google.dagger.hilt.android") version "2.48"
    }

    App-level build.gradle

    Apply the plugin and add the Hilt library dependency along with kapt (the Kotlin annotation processor for compile-time code generation):
    plugins {
        id("com.google.dagger.hilt.android")
        // … other plugins
    }
    
    android {
        // … existing configuration
    }
    
    dependencies {
        implementation("com.google.dagger:hilt-android:2.48")
        kapt("com.google.dagger:hilt-android-compiler:2.48")
    }
    
    kapt {
        correctErrorTypes = true
    }
    The correctErrorTypes = true flag tells kapt to run a second pass that replaces generated subclass names in error messages, making compiler errors far more readable during development.

    Application class

    Annotate your Application class with @HiltAndroidApp. This is mandatory — omitting it means Hilt never initializes, and no injection will occur:
    @HiltAndroidApp
    class MyApplication : Application()
    
    // In AndroidManifest.xml:
    // <application android:name=".MyApplication" … />
    @HiltAndroidApp triggers Hilt's code generation at compile time, producing a top-level DaggerApplication component that serves as the entry point for the entire dependency graph. From this single annotation, Hilt knows how to build and manage the graph for your entire app.

    Core Concepts: Injection, Modules, and Component Scopes

    Once the project is configured, the three core concepts you work with day-to-day are injection types, modules, and component scopes. Understanding how these three pieces fit together lets you design a dependency graph that is both easy to reason about and efficient at runtime.

    Constructor injection

    Constructor injection — marking a class's constructor with @Inject — is the recommended approach in Hilt. It keeps dependencies explicit, makes the class impossible to instantiate without satisfying its contract, and works seamlessly with the generated Dagger component:
    class UserRepository @Inject constructor(
        private val localDataSource: UserLocalDataSource,
        private val remoteDataSource: UserRemoteDataSource
    ) {
        // …
    }
    When Hilt processes this constructor, it automatically resolves UserLocalDataSource and UserRemoteDataSource from the graph and passes them in. Field injection (annotating fields directly) is still supported but is considered a fallback — use it only when you need to inject into Android framework classes that you cannot modify, such as a View or a legacy Activity.

    Providing dependencies with modules

    Not everything can be annotated with @Inject — third-party classes, interface implementations, and objects requiring custom construction logic all need an explicit provider. Hilt modules are classes annotated with @Module that live inside a @Module-annotated companion object and expose dependencies via @Provides or @Binds. Use @Provides for any class you want to construct with custom logic:
    @Module
    @InstallIn(SingletonComponent::class)
    object NetworkModule {
    
        @Provides
        @Singleton
        fun provideOkHttpClient(): OkHttpClient {
            return OkHttpClient.Builder()
                .connectTimeout(30, TimeUnit.SECONDS)
                .readTimeout(30, TimeUnit.SECONDS)
                .build()
        }
    
        @Provides
        @Singleton
        fun provideRetrofit(okHttpClient: OkHttpClient): Retrofit {
            return Retrofit.Builder()
                .client(okHttpClient)
                .baseUrl("https://api.example.com/")
                .build()
        }
    }
    Use @Binds when you have an interface and a concrete implementation — @Binds is more efficient because Dagger generates a slim binding instead of a full factory:
    @Module
    @InstallIn(SingletonComponent::class)
    abstract class RepositoryModule {
    
        @Binds
        @Singleton
        abstract fun bindUserRepository(
            impl: UserRepositoryImpl
        ): UserRepository
    }

    Component scopes

    @InstallIn determines which Hilt component a module belongs to, and therefore how long the dependencies it provides live. Hilt ships with a set of predefined component scopes that mirror Android lifecycle boundaries:
    Component Scope annotation Typical use case
    SingletonComponent @Singleton App-wide singletons: network clients, repositories, databases
    ActivityRetainedComponent @ActivityRetainedScoped Data that survives configuration changes (survives process death)
    ViewModelComponent @ViewModelScoped Dependencies tied to a ViewModel’s lifecycle
    ActivityComponent @ActivityScoped Dependencies shared across fragments in a single activity
    FragmentComponent @FragmentScoped Dependencies scoped to a single fragment
    ServiceComponent @ServiceScoped Dependencies scoped to a Service
    The most commonly used scope is @Singleton on the SingletonComponent — objects here are created once per application lifecycle. More granular scopes like @ViewModelScoped are useful when you want to share a dependency across multiple fragments but not across the entire app, keeping memory usage tight and avoiding accidental cross-screen state leakage.

    Practical Takeaways: Entry Points and Jetpack Integrations

    @AndroidEntryPoint and framework class injection

    The @AndroidEntryPoint annotation is what unlocks Hilt inside Android framework classes. Without it, Hilt has no way to generate a Hilt-enhanced version of an Activity, Fragment, Service, or BroadcastReceiver, and any attempt to inject into those classes will fail at compile time. Applying @AndroidEntryPoint is straightforward — add the annotation to the class declaration:
    @AndroidEntryPoint
    class MainActivity : AppCompatActivity() {
    
        @Inject
        lateinit var analytics: AnalyticsService
    
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            // analytics is ready to use
        }
    }
    For Fragment, note that you must also add @AndroidEntryPoint to any nested fragments if you want them to receive injected dependencies, and you should inject in onAttach or onCreate — not in the constructor — to avoid accessing injected fields before they are assigned.

    @HiltViewModel for ViewModels

    The @HiltViewModel annotation replaced the deprecated @ViewModelInject and is now the standard way to have Hilt manage your ViewModel dependencies. Any ViewModel annotated with @HiltViewModel automatically gets its constructor parameters satisfied from the Hilt graph:
    @HiltViewModel
    class UserListViewModel @Inject constructor(
        private val repository: UserRepository,
        private val savedStateHandle: SavedStateHandle
    ) : ViewModel() {
        // …
    }
    To retrieve a @HiltViewModel-annotated ViewModel, use the by viewModels() delegate from the hilt-android library in your Activity or Fragment — the delegate handles the Hilt-specific factory wiring under the hood.

    Jetpack integrations

    Hilt ships with official integrations for several Jetpack libraries. All of them follow the same component scoping model and require only an additional Gradle dependency and a small amount of setup code. WorkManager. Add hilt-work to your dependencies and override Configuration with a HiltWorkerFactory:
    // In your Application class
    @HiltAndroidApp
    class MyApplication : Application(), Configuration.Provider {
        @Inject
        lateinit var workerFactory: HiltWorkerFactory
    
        override val workManagerConfiguration: Configuration
            get() = Configuration.Builder()
                .setWorkerFactory(workerFactory)
                .build()
    }
    
    // In a Worker class
    @HiltWorker
    class SyncWorker @AssistedInject constructor(
        @Assisted appContext: Context,
        @Assisted workerParams: WorkerParameters,
        private val repository: UserRepository
    ) : CoroutineWorker(appContext, workerParams) {
        // …
    }
    Navigation Component. Add hilt-navigation-fragment and hilt-navigation-ui. Once the dependencies are in place, Hilt automatically provides a NavBackStackEntry-aware ViewModelFactory so that scoped ViewModels survive navigation graph changes:
    // In NavHostFragment
    val navController = navHostFragment.navController
    val viewModel: MyViewModel = navController
        .currentBackStackEntry
        ?.let { entry ->
            ViewModelProvider(entry, viewModelFactory)[MyViewModel::class.java]
        }
    }
    Jetpack Compose. Add hilt-navigation-compose and use @HiltComposeEntryPoint inside Compose composables to access the DI graph directly, or rely on the hiltViewModel() composable to delegate ViewModel creation:
    @Composable
    fun UserListScreen(
        viewModel: UserListViewModel = hiltViewModel()
    ) {
        // viewModel is fully injected and survives recomposition
    }
    All three integrations follow the same principle: Hilt handles the factory and scope management, while the Jetpack library handles its own lifecycle. The result is a clean separation of concerns where each layer does what it is best at.

    Best Practices, Testing, and Common Pitfalls

    Best practices

    A few patterns consistently separate well-managed Hilt projects from messy ones:
    • Prefer constructor injection over field injection. It makes dependencies explicit, unit-testable without special setup, and impossible to forget during code review.
    • Keep modules focused and single-responsibility. A NetworkModule that provides OkHttpClient, Retrofit, and API services is easier to maintain than a catch-all AppModule.
    • Use the narrowest scope that makes sense. @Singleton is convenient but can hide lifecycle bugs. Prefer @ViewModelScoped for UI-related state that should not leak across screens.
    • Always set correctErrorTypes = true in kapt. Without it, kapt-generated error messages reference synthetic subclass names that are nearly impossible to map back to your source code.
    • Do not create your own component builders. Hilt generates the component hierarchy automatically. Customizing it breaks the automatic scoping model and breaks Jetpack integrations that rely on the standard components.

    Testing with Hilt

    Hilt generates test-specific components automatically. For unit tests, the @HiltTest� annotation from hilt-android-testing creates an isolated test component before each test and tears it down after:
    @HiltAndroidTest
    class UserRepositoryTest {
    
        @Inject
        lateinit var repository: UserRepository
    
        @Test
        fun `repository returns users from network`(): Unit = runTest {
            // repository is a fully wired instance from the test graph
        }
    }
    For UI tests, @AndroidEntryPoint on your test Activities and Fragments works exactly as it does in production — Hilt generates the corresponding test components behind the scenes. This means your espresso tests get the same DI graph your app uses, without any manual factory wiring.

    Migration from dagger.android

    Projects that used dagger.android (the pre-Hilt Android-specific Dagger extensions) have a documented migration path. The core strategy is to replace @ContributesAndroidInjector-generated subcomponents with standard Hilt modules, replace @AndroidInjector.Builder implementations with @AndroidEntryPoint, and remove DispatchingAndroidInjector entirely. Google's official Hilt documentation covers the full migration in detail.

    Common pitfalls

    • Forgetting @HiltAndroidApp on the Application class. This is the single most common setup mistake. Without it, no component is generated and nothing injects.
    • Mismatched scope annotations. Injecting a @Singleton dependency into a @ViewModelScoped component will compile but violates the scope hierarchy — the singleton lives longer than the ViewModel, which can cause subtle bugs when the ViewModel-specific state unexpectedly persists.
    • Missing kapt plugin. Without it, the annotation processor never runs and no code is generated, leading to cryptic runtime errors about missing bindings.
    • Reinjecting the same dependency at multiple scopes. If you declare a binding in SingletonComponent and also in ActivityComponent, Hilt uses the most specific scope at each injection site, which can lead to two different instances being created unexpectedly.

    FAQ

    What is the difference between @Provides and @Binds in Hilt?

    @Provides@Bindsabstractabstract@Binds

    Do I need to use field injection or constructor injection with Hilt?

    @InjectActivityFragment@AndroidEntryPoint

    Can Hilt be used with Jetpack Compose?

    hilt-navigation-composehiltViewModel()@HiltComposeEntryPoint Hilt transforms Android dependency injection from a notorious pain point into a routine part of your development workflow. The initial setup takes minutes, the compile-time model catches errors before they reach a device, and the suite of Jetpack integrations means you rarely need to write custom factories or manual wiring code again. Start with constructor injection and @Inject, define your external dependencies in modules, scope them appropriately, and reach for @AndroidEntryPoint and @HiltViewModel whenever you need Hilt to manage Android framework classes. From there, adding WorkManager workers, Navigation graphs, or Compose screens is a matter of adding a dependency and following the library-specific annotation — not redesigning your entire DI architecture.