Hilt for Android: DI Mastery in 2026

Listen to this article · 14 min listen

Key Takeaways

  • Get your project configured for Hilt by adding its dependencies and the Kapt/KSP plugin to your module’s build.gradle file.
  • You have to annotate your `Application` class with `@HiltAndroidApp` to kick off Hilt’s code generation and component setup.
  • To get dependencies into Android components like Activities, use the `@AndroidEntryPoint` annotation on the class and `@Inject` on the constructor or field.
  • For things like interfaces or classes from a third-party library, define custom bindings in a class marked with `@Module` and use `@Provides` functions so Hilt knows how to create them.
  • Manage the lifecycle of your dependencies with scoping annotations like `@Singleton` to control how they’re reused and save on resources.

If you want to build Android apps that don’t fall apart as they grow, you’re going to need dependency injection (DI). Using a DI framework means you stop manually creating every object your classes need. Instead, a central container handles all that complex object graph management, which cuts down on boilerplate and forces better code organization. You hand off the responsibility of creating and managing object instances to this container, which makes your individual components more independent and, critically, way easier to write unit tests for. This whole approach rebuilds your app’s structure around loosely coupled, modular designs instead of the old tightly coupled messes. But how do you get started with this in a real Android project?

1. Project Setup: Integrating Hilt into Your Android Application

First things first, you have to get a DI framework into your project. Hilt, which is Google’s recommended library, is built right on top of Dagger but strips away a ton of the complexity, making it a much better starting point for most Android developers. To get going, you’ll need to edit your project’s build.gradle files. I’ve seen a lot of people get stuck on this initial setup, usually because they miss a plugin or a specific dependency. Getting this configuration right from the start is absolutely required for Hilt’s code generation to work at all.

Start by adding the Hilt plugin classpath to your project-level build.gradle file, which you’ll find at the root of your project directory.

buildscript { ext { hilt_version = '2.50' // Use the latest stable version } repositories { google() mavenCentral() } dependencies { classpath 'com.android.tools.build:gradle:8.2.2' // Your current Android Gradle Plugin version classpath "com.google.dagger:hilt-android-gradle-plugin:$hilt_version" }
} plugins { id 'com.android.application' version '8.2.2' apply false id 'com.android.library' version '8.2.2' apply false id 'org.jetbrains.kotlin.android' version '1.9.22' apply false
}

With that done, jump over to your module-level build.gradle file (which is probably at app/build.gradle) and apply the Hilt plugin there, along with adding the actual Hilt dependencies.

plugins { id 'com.android.application' id 'org.jetbrains.kotlin.android' id 'kotlin-kapt' // Or 'com.google.devtools.ksp' for KSP id 'com.google.dagger.hilt.android'
} android { // ...
} dependencies { implementation "com.google.dagger:hilt-android:$hilt_version" kapt "com.google.dagger:hilt-android-compiler:$hilt_version" // Use kapt or ksp // If using KSP (Kotlin Symbol Processing), replace kapt with ksp: // ksp "com.google.dagger:hilt-android-compiler:$hilt_version" // Other dependencies implementation 'androidx.core:core-ktx:1.12.0' implementation 'androidx.appcompat:appcompat:1.6.1' implementation 'com.google.android.material:material:1.11.0' implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
}

Once you’ve made these changes, sync your project with Gradle. You should see Hilt’s annotation processors kick in which can take a few seconds.

Pro Tip: KSP vs. Kapt

While kapt was the go-to for Kotlin annotation processing for years, Google is now pushing everyone toward Kotlin Symbol Processing (KSP). KSP can give you a serious build speed boost, I’ve seen it cut compilation times by 20% to 30% on big projects. If you’re starting a new Kotlin project, I’d strongly recommend going straight for com.google.devtools.ksp instead of kotlin-kapt and just switching out the dependency line. As we’re heading toward 2026, most major libraries like Hilt have first-class KSP support, so it’s the obvious choice for better performance.

2. The Application Class: The Entry Point for Hilt

After you’ve integrated the Hilt library, you need to give it a starting point for building its dependency graph. You do this by annotating your custom Application class. If your project doesn’t have one yet, you’ll have to create it. This class acts as the root of the dependency tree, making sure that every component in your app can get the dependencies that Hilt manages.

Go ahead and create a new Kotlin class, let’s call it MyApplication.kt:

package com.example.myapp import android.app.Application
import dagger.hilt.android.HiltAndroidApp @HiltAndroidApp
class MyApplication : Application() { override fun onCreate() { super.onCreate() // Any other application-wide setup can go here }
}

Next, you absolutely must remember to declare this custom Application class in your AndroidManifest.xml file.

<application android:name=".MyApplication" android:allowBackup="true" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true" android:theme="@style/Theme.MyApp"> <activity android:name=".MainActivity"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity>
</application>

That @HiltAndroidApp annotation is what tells Hilt to start its code generation which spits out a base class that handles setting up the main dependency component. If you forget that annotation, Hilt has no idea where to begin.

Common Mistake: Forgetting AndroidManifest.xml

I’ve seen it a dozen times: a developer creates the @HiltAndroidApp class but then completely forgets to update the android:name attribute inside the <application> tag in the AndroidManifest.xml. If you make this mistake, your app will just launch with the default Application class, Hilt will never get initialized, and you’ll get a nasty runtime crash the first time you try to inject something. Always, always double-check your manifest after setting up a custom Application class.

3. Injecting Dependencies into Android Components

With Hilt properly configured, you can finally start injecting dependencies into your Android components, your Activities, Fragments, Services, and so on. Hilt gives you a set of annotations for this that make the process way easier than it was with old-school Dagger setups.

To get a dependency into an Activity, for example, you first have to annotate the class with @AndroidEntryPoint. This annotation is the flag that tells Hilt to scan this component for injection requests. Then, you just use the @Inject annotation on any field you want Hilt to provide an instance for.

Let’s imagine you have a simple Logger class that you want to use inside your MainActivity:

// Logger.kt
package com.example.myapp.utils import android.util.Log
import javax.inject.Inject
import javax.inject.Singleton @Singleton // Scopes this logger to the application's lifecycle
class Logger @Inject constructor() { fun log(message: String) { Log.d("MyApp", message) }
}
// MainActivity.kt
package com.example.myapp import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import dagger.hilt.android.AndroidEntryPoint
import com.example.myapp.utils.Logger
import javax.inject.Inject @AndroidEntryPoint
class MainActivity : AppCompatActivity() { @Inject lateinit var logger: Logger // Hilt will provide an instance of Logger override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) logger.log("MainActivity created!") }
}

And that’s it. Hilt sees the @Inject on the `logger` field and automatically provides an instance of the Logger class. It knows how to create a `Logger` because of the @Inject constructor() annotation on the `Logger` class itself. As long as Hilt knows how to create all the parameters in a constructor (or if there are none, like here), it can handle the injection. This is constructor injection in a nutshell.

4. Defining Custom Bindings with Hilt Modules

You can’t use constructor injection for everything. What about interfaces, classes from a third-party library, or classes that need something like a Context passed into their constructor? Hilt has no way of knowing how to create those out of the box. This is exactly what Hilt Modules are for.

A Hilt Module is just a class that you annotate with @Module. Inside it, you write functions with the @Provides annotation to tell Hilt, “Hey, when someone asks for this type, here’s how you build it.” You also have to tell Hilt what component’s lifecycle this module belongs to by using @InstallIn. For dependencies you want to be available everywhere, you’ll install them in the SingletonComponent::class.

Let’s say your app uses an AnalyticsService interface with a concrete implementation called FirebaseAnalyticsService:

// AnalyticsService.kt
package com.example.myapp.analytics interface AnalyticsService { fun trackEvent(eventName: String, params: Map<String, String>? = null)
}
// FirebaseAnalyticsService.kt
package com.example.myapp.analytics import android.content.Context
import android.os.Bundle
import com.google.firebase.analytics.FirebaseAnalytics
import javax.inject.Inject
import javax.inject.Singleton class FirebaseAnalyticsService @Inject constructor( private val context: Context // Requires Context, which Hilt can provide
) : AnalyticsService { private val firebaseAnalytics: FirebaseAnalytics = FirebaseAnalytics.getInstance(context) override fun trackEvent(eventName: String, params: Map<String, String>?) { val bundle = params?.let { val b = Bundle() it.forEach { (key, value) -> b.putString(key, value) } b } firebaseAnalytics.logEvent(eventName, bundle) }
}

Now, you need a module to tell Hilt that when someone requests an AnalyticsService, it should provide a FirebaseAnalyticsService.

// AppModule.kt
package com.example.myapp.di import android.content.Context
import com.example.myapp.analytics.AnalyticsService
import com.example.myapp.analytics.FirebaseAnalyticsService
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton @Module
@InstallIn(SingletonComponent::class) // This module's dependencies live as long as the application
object AppModule { // Use object for stateless modules @Singleton // Ensures only one instance of FirebaseAnalyticsService throughout the app @Provides fun provideAnalyticsService( @ApplicationContext context: Context // Hilt provides ApplicationContext ): AnalyticsService { return FirebaseAnalyticsService(context) }
}

With this module in place, any class in your app can just @Inject an AnalyticsService, and Hilt will know to provide the FirebaseAnalyticsService instance, even though it’s an interface. Notice the @ApplicationContext qualifier. That’s a special Hilt binding that gives you the application’s context, which is a very common pattern for providing Android-specific dependencies like this one.

Pro Tip: Scoping Dependencies

You probably noticed the @Singleton annotation on both the Logger class and the provideAnalyticsService function. Pay attention to scoping, because it’s a core DI concept. Without it, Hilt would create a brand new instance of `Logger` or `FirebaseAnalyticsService` every single time you inject it somewhere, which is a huge waste of resources. The `@Singleton` annotation tells Hilt to create just one instance for the entire lifecycle of the component it’s installed in (the whole app, in this case). Hilt also gives you other scopes like @ActivityScoped and @FragmentScoped that tie the dependency’s lifetime to the lifecycle of an Activity or Fragment. Using the right scope is how you prevent memory leaks and keep your app efficient. For example, a Retrofit client is a perfect candidate for @Singleton, whereas something specific to a screen might be @ActivityRetainedScoped.

5. Injecting ViewModels with Hilt

ViewModels are at the heart of any modern Android app, and Hilt has fantastic support for injecting them. This integration makes creating ViewModels, especially those with their own dependencies, incredibly simple.

First, you’ll need to add another Hilt dependency for ViewModels to your module-level build.gradle file:

dependencies { // ... other dependencies implementation 'androidx.hilt:hilt-navigation-fragment:1.1.0' // For fragments implementation 'androidx.hilt:hilt-navigation-compose:1.1.0' // For Compose implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.7.0' // ViewModel ktx kapt 'androidx.hilt:hilt-compiler:1.1.0' // Hilt compiler for ViewModel // If using KSP, replace kapt with ksp: // ksp 'androidx.hilt:hilt-compiler:1.1.0'
}

Next, create your ViewModel and just annotate its class with @HiltViewModel. This triggers Hilt to generate a factory behind the scenes, so you can inject dependencies directly into the ViewModel’s constructor just like any other class.

// MyViewModel.kt
package com.example.myapp.ui.main import androidx.lifecycle.ViewModel
import dagger.hilt.android.lifecycle.HiltViewModel
import com.example.myapp.data.UserRepository
import com.example.myapp.utils.Logger
import javax.inject.Inject @HiltViewModel
class MyViewModel @Inject constructor( private val userRepository: UserRepository, private val logger: Logger
) : ViewModel() { // ViewModel logic fun fetchData() { logger.log("Fetching data from UserRepository...") userRepository.getUsers() }
}

In this example, both a UserRepository and our Logger are injected right into the MyViewModel. You’d probably define the provider for `UserRepository` in a Hilt module, since it’s likely an interface.

Now, to get an instance of this ViewModel in your Activity or Fragment, you just use the viewModels() property delegate:

// MainActivity.kt (or a Fragment)
package com.example.myapp import android.os.Bundle
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import dagger.hilt.android.AndroidEntryPoint
import com.example.myapp.ui.main.MyViewModel @AndroidEntryPoint
class MainActivity : AppCompatActivity() { private val viewModel: MyViewModel by viewModels() override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) viewModel.fetchData() }
}

When you use the by viewModels() delegate inside a class annotated with @AndroidEntryPoint, it automatically hooks into Hilt to get the ViewModel instance. This is a massive improvement over the old days of writing custom `ViewModelFactory` classes by hand, which was a huge source of boilerplate and silly mistakes. As Google’s own developer blog points out, this is a key piece of their recommended architecture for building testable, modular apps.

Common Mistake: Missing ViewModel Compiler

A classic mistake is forgetting to add the specific `hilt-compiler` dependency for ViewModels (either `kapt ‘androidx.hilt:hilt-compiler:1.1.0’` or `ksp ‘androidx.hilt:hilt-compiler:1.1.0’`). If you miss this, your classes annotated with `@HiltViewModel` won’t be processed correctly, and you’ll get build errors or runtime crashes complaining about a missing ViewModel factory. Just make sure all your Hilt compiler plugins are there and configured correctly.

Bringing dependency injection with Hilt into your Android app fundamentally changes how you think about creating objects and managing their lifecycles. It pushes you toward a modular architecture that makes your codebase stronger and much easier to test. Adopting this pattern is a strategic decision for the long-term health and scalability of your projects. For instance, building a solid enterprise mobile strategy will increasingly depend on these kinds of strong architectural patterns. It also gives you a stable foundation for testing and iteration, helping you avoid common mobile experimentation failures.

What is dependency injection and why is it used in Android?

It’s a design pattern where a class gets its dependencies (other objects it needs to work) from an outside source instead of creating them itself. In Android development, this cuts down on boilerplate code, makes your classes easier to test because you can swap in mock dependencies, and generally leads to a cleaner, more modular app structure.

What is the difference between Hilt and Dagger?

Hilt is built on top of Dagger to standardize and simplify its use specifically for Android. Dagger is a powerful, general-purpose DI framework, but it requires a lot of manual setup and boilerplate code, especially to make it work with Android’s component lifecycles. Hilt automates most of that by providing pre-made components and scopes for standard Android classes, making it much easier to get started.

When should I use @ApplicationContext versus just Context in a Hilt module?

You should pretty much always use @ApplicationContext when you need a Context inside a Hilt module, especially for singleton-scoped dependencies. This special qualifier guarantees you get the application’s singleton Context, which is safe to hold a reference to. If you just ask for an unqualified Context, Hilt might give you an Activity’s context which can easily lead to memory leaks if you hold onto it past the Activity’s destruction.

Can Hilt inject dependencies into custom views?

No, not directly. Hilt’s built-in support is for Android framework components like Application, Activity, Fragment, Service, and BroadcastReceiver. If you have a custom View that needs dependencies, the common pattern is to pass them in through its constructor or use a factory, where the factory itself can get its dependencies from Hilt.

How does Hilt handle different lifecycles for dependencies?

Hilt uses scoping annotations to tie a dependency’s lifecycle to a specific component. For example, @Singleton makes a dependency live for as long as the entire application. @ActivityScoped creates an instance that lasts for the life of an Activity, and @FragmentScoped does the same for a Fragment. These scopes are how you make sure objects are created and garbage collected at the right times, preventing memory leaks and optimizing performance.

Courtney Green

Lead Developer Experience Strategist M.S., Human-Computer Interaction, Carnegie Mellon University

Courtney Green is a Lead Developer Experience Strategist with 15 years of experience specializing in the behavioral economics of developer tool adoption. She previously led research initiatives at Synapse Labs and was a senior consultant at TechSphere Innovations, where she pioneered data-driven methodologies for optimizing internal developer platforms. Her work focuses on bridging the gap between engineering needs and product development, significantly improving developer productivity and satisfaction. Courtney is the author of "The Engaged Engineer: Driving Adoption in the DevTools Ecosystem," a seminal guide in the field