Physical Address
304 North Cardinal St.
Dorchester Center, MA 02124
Physical Address
304 North Cardinal St.
Dorchester Center, MA 02124

As an Android application grows, its codebase usually becomes more complex. Classes begin depending on repositories, repositories depend on APIs or databases, ViewModels depend on repositories, and different parts of the application may require shared services such as network clients, analytics, or local storage.
Without a clear strategy for managing these relationships, developers can end up creating objects manually throughout the application. This can lead to tightly coupled code, difficult testing, duplicated configuration, and complicated maintenance.
This is where Dependency Injection (DI) becomes valuable.
Dependency Injection is a software design technique that allows a class to receive the objects it needs rather than creating those objects itself. On Android, Hilt provides a modern, standardized way to implement dependency injection using the Dagger framework.
Google’s Android documentation recommends Hilt for dependency injection in Android applications because it provides standard containers and integrates with Android’s lifecycle-aware components.
In this guide, we will explore Hilt dependency injection in Android, understand why it matters, and learn how to structure a maintainable application using Hilt.
Before learning Hilt, it is important to understand the underlying concept.
Suppose you have a UserRepository that needs an API service.
Without dependency injection, you might write:
class UserRepository {
private val api = UserApi()
fun getUsers() {
api.getUsers()
}
}
The repository creates its own dependency.
This seems simple, but it creates tight coupling.
The repository now knows exactly how UserApi is constructed.
With dependency injection, the dependency can be provided from outside:
class UserRepository(
private val api: UserApi
)
Now the repository does not need to know how UserApi is created.
Something else is responsible for providing it.
This separation is the fundamental idea behind Dependency Injection Android development.
Developers can implement dependency injection manually, but large Android applications often have many dependencies and complex object graphs.
Hilt helps automate the process.
Some benefits include:
Hilt is built on top of Dagger and provides Android-specific integrations that simplify dependency injection configuration.
Hilt and Dagger are closely related, but they are not exactly the same thing.
Dagger is a dependency injection framework that provides compile-time dependency injection.
Hilt builds on Dagger and adds Android-specific functionality.
Hilt provides predefined components and scopes for common Android application components.
This means developers do not have to manually configure every part of the dependency graph.
For many Android projects, Hilt provides a simpler entry point into Dagger-based dependency injection.
A simplified Hilt architecture looks like this:
Application
↓
Hilt Components
↓
Modules
↓
Dependencies
↓
Activities / Fragments / ViewModels
For example:
Hilt
↓
Network Module
↓
Retrofit
↓
ApiService
↓
Repository
↓
ViewModel
↓
UI
The application does not need to manually create every object.
Hilt manages the dependency graph based on the configuration you provide.
The exact Gradle configuration depends on the Android Gradle Plugin and Hilt version used by the project.
Modern Android projects commonly configure Hilt through Gradle plugins and dependencies.
The important dependencies include Hilt’s Android library and compiler.
For example, a project may use a configuration similar to:
dependencies {
implementation("com.google.dagger:hilt-android:<version>")
kapt("com.google.dagger:hilt-compiler:<version>")
}
The current official Android documentation should be used when configuring versions because build tools and dependency-management recommendations evolve over time.
Hilt needs an Android application class annotated with @HiltAndroidApp.
For example:
@HiltAndroidApp
class MyApplication : Application()
This annotation triggers Hilt’s code generation and establishes the application-level dependency container.
The application class must also be registered in the Android manifest:
<application
android:name=".MyApplication"
... >
</application>
Once this is configured, Hilt can begin managing dependencies for the application.
One of the most important Hilt concepts is constructor injection.
Suppose you have a repository:
class UserRepository(
private val apiService: UserApiService
)
You can tell Hilt to provide the class using:
class UserRepository @Inject constructor(
private val apiService: UserApiService
)
The @Inject annotation tells Hilt that this constructor can be used to create the object.
If Hilt already knows how to provide UserApiService, it can construct UserRepository automatically.
This is usually the preferred approach when you control the class and its constructor dependencies.
Constructor injection offers several advantages.
A class clearly shows what it requires:
class UserRepository @Inject constructor(
private val api: UserApiService,
private val database: UserDatabase
)
Anyone reading the class immediately knows its dependencies.
You can provide fake implementations during tests.
val repository = UserRepository(
fakeApi
)
The class focuses on its job rather than figuring out how all of its dependencies should be constructed.
Constructor injection does not work for every dependency.
You may need to provide objects from:
This is where Hilt modules become useful.
For example:
@Module
@InstallIn(SingletonComponent::class)
object NetworkModule {
@Provides
fun provideApiService(): UserApiService {
return Retrofit.Builder()
.baseUrl("https://example.com/")
.build()
.create(UserApiService::class.java)
}
}
The module tells Hilt how to create an object that cannot simply use constructor injection.
Android’s official Hilt documentation explains @Module, @Provides, and component installation as core parts of dependency configuration.
@Provides vs @BindsTwo important Hilt annotations are @Provides and @Binds.
@ProvidesUse @Provides when you need to manually construct an object.
For example:
@Provides
fun provideRetrofit(): Retrofit {
return Retrofit.Builder()
.baseUrl(BASE_URL)
.build()
}
@BindsUse @Binds when you want to tell Hilt that an implementation should be used for an interface.
For example:
interface UserRepository {
fun getUsers()
}
Implementation:
class UserRepositoryImpl @Inject constructor(
private val api: UserApiService
) : UserRepository {
override fun getUsers() {
// Implementation
}
}
Module:
@Module
@InstallIn(SingletonComponent::class)
abstract class RepositoryModule {
@Binds
abstract fun bindUserRepository(
implementation: UserRepositoryImpl
): UserRepository
}
This allows the rest of the application to depend on the abstraction:
class UserViewModel @Inject constructor(
private val repository: UserRepository
) : ViewModel()
This approach improves flexibility and testability.
One of Hilt’s most powerful features is its predefined component hierarchy.
Hilt provides components associated with Android application lifecycles.
Important components include:
SingletonComponentActivityRetainedComponentViewModelComponentActivityComponentFragmentComponentViewComponentViewWithFragmentComponentServiceComponentEach component has an associated lifecycle.
For example, SingletonComponent lives as long as the application.
A dependency installed there can be shared across the application.
Example:
@Provides
@Singleton
fun provideRetrofit(): Retrofit {
return Retrofit.Builder()
.baseUrl(BASE_URL)
.build()
}
The @Singleton scope tells Hilt that the dependency should use a single instance within the corresponding component.
Developers should choose scopes carefully because unnecessarily long-lived objects can consume resources.
Hilt integrates directly with Android ViewModel.
A ViewModel can receive dependencies through constructor injection:
@HiltViewModel
class UserViewModel @Inject constructor(
private val repository: UserRepository
) : ViewModel() {
fun loadUsers() {
// Load users
}
}
Then an Activity or Fragment can obtain the ViewModel using the appropriate Android APIs.
This creates a clean dependency chain:
UI
↓
UserViewModel
↓
UserRepository
↓
UserApiService
↓
Retrofit
The UI does not need to construct the repository or API service manually.
Android’s official Hilt documentation provides dedicated guidance for injecting dependencies into ViewModels.
Networking is one of the most common places to use Hilt.
A typical architecture may include:
@Module
@InstallIn(SingletonComponent::class)
object NetworkModule {
@Provides
@Singleton
fun provideRetrofit(): Retrofit {
return Retrofit.Builder()
.baseUrl(BASE_URL)
.build()
}
@Provides
@Singleton
fun provideUserApi(
retrofit: Retrofit
): UserApiService {
return retrofit.create(UserApiService::class.java)
}
}
Now Hilt understands that UserApiService depends on Retrofit.
A repository can simply request the API:
class UserRepository @Inject constructor(
private val api: UserApiService
)
This is a good example of dependency injection reducing object-creation code throughout the application.
Hilt can also provide Room database dependencies.
A simplified example:
@Module
@InstallIn(SingletonComponent::class)
object DatabaseModule {
@Provides
@Singleton
fun provideDatabase(
application: Application
): AppDatabase {
return Room.databaseBuilder(
application,
AppDatabase::class.java,
"app_database"
).build()
}
}
A DAO can then be provided from the database.
This allows repositories to request the DAO without knowing how the database itself is constructed.
Sometimes an application needs multiple objects of the same type.
For example, you might have two Retrofit instances:
Hilt needs a way to distinguish between them.
This is where qualifiers are useful.
For example:
@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class AuthRetrofit
You can then annotate a provider:
@Provides
@AuthRetrofit
fun provideAuthRetrofit(): Retrofit {
// Configuration
}
The qualifier tells Hilt which dependency should be injected.
Qualifiers are particularly useful in larger applications with multiple implementations or configurations of the same type.
Dependency injection becomes especially valuable when writing tests.
Imagine a repository depends on a real API.
In production:
Repository
↓
Real API
During a test, you may want:
Repository
↓
Fake API
This makes it possible to test business logic without relying on a real network connection.
Hilt provides testing support that allows developers to replace dependencies in test environments.
A good dependency injection architecture should make it easy to replace external dependencies such as:
Hilt is powerful, but it does not automatically create good architecture.
A project can become difficult to understand if every dependency is placed into one enormous module.
Instead, organize modules by responsibility.
For example:
di/
├── NetworkModule.kt
├── DatabaseModule.kt
├── RepositoryModule.kt
└── AnalyticsModule.kt
Not every dependency needs to be a singleton.
Long-lived objects can consume resources unnecessarily.
Choose the smallest appropriate lifecycle scope.
Dependency injection does not mean every class needs ten dependencies.
If a class has an excessive number of dependencies, it may indicate that the class has too many responsibilities.
Avoid service locators or global access patterns when constructor injection can express dependencies clearly.
A constructor like:
class UserRepository @Inject constructor(
private val api: UserApiService
)
is much easier to understand than a class that silently retrieves dependencies from a global container.
For a scalable project, consider these recommendations:
Use constructor injection whenever possible.
Use @Provides and @Binds when constructor injection is not appropriate.
Group related dependencies together.
Repositories and services can depend on interfaces when multiple implementations may be needed.
Match the dependency’s lifetime to its actual requirements.
A singleton is not automatically better or faster.
Hilt can inject dependencies into ViewModels, but the ViewModel should still have a clear responsibility.
Make it easy to replace real services with fake or test implementations.
Dependency injection should support good architecture rather than replace it.
A modern Android application might follow this structure:
UI
│
├── Activity / Fragment / Compose
│
↓
ViewModel
│
↓
Repository Interface
│
↓
Repository Implementation
│
├── API
│
└── Database
Hilt manages the connections:
Hilt
│
┌────────┼─────────┐
↓ ↓ ↓
ViewModel Repository Network
│ │ │
└────────┴─────────┘
This architecture makes object creation centralized while allowing individual components to remain focused.
Hilt is particularly useful when:
For a tiny application with only a few classes, manual dependency injection may be sufficient.
The goal should not be to introduce Hilt simply because it is popular.
The goal is to use dependency injection when it solves a real architectural problem.
Mastering Dependency Injection in Android using Hilt is an important step toward building scalable and maintainable Android applications.
Hilt reduces the complexity of creating and connecting application dependencies while integrating naturally with Android components such as Application, Activity, Fragment, and ViewModel.
By using constructor injection, modules, scopes, qualifiers, and interfaces appropriately, developers can create a clear dependency graph that is easier to understand and test.
The most important lesson is that Hilt itself is not the architecture. It is a tool that supports good architecture.
A well-designed Android application should have clear responsibilities, controlled dependencies, sensible lifecycles, and strong testability. Hilt helps make those principles easier to implement.
For developers working on modern Kotlin and Android projects, understanding Hilt dependency injection, Hilt modules, Hilt ViewModel integration, dependency scopes, and Hilt testing can significantly improve the quality and maintainability of their applications.
Start with simple constructor injection, introduce modules only when necessary, keep scopes under control, and continuously evaluate whether each dependency belongs where it is being injected.
With these practices, Hilt becomes more than a dependency-management library. It becomes a practical foundation for building Android applications that are easier to develop, test, scale, and maintain.