Mastering Dependency Injection in Android Using Hilt A Complete Guide for Modern Android Development

Mastering Dependency Injection in Android Using Hilt: A Complete Guide for Modern Android Development 2026

Mastering Dependency

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.


What Is Dependency Injection in Android?

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.


Why Use Hilt for Android Dependency Injection?

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:

  • Less boilerplate
  • Better separation of concerns
  • Lifecycle-aware dependency scopes
  • Easier testing
  • Centralized dependency configuration
  • Integration with Android components
  • Support for ViewModel
  • Support for Activities and Fragments
  • Integration with Jetpack libraries

Hilt is built on top of Dagger and provides Android-specific integrations that simplify dependency injection configuration.


Hilt vs Dagger

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.


How Hilt Dependency Injection Works

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.


Step 1: Add Hilt to Your Android Project

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.


Step 2: Create the Application Class

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.


Step 3: Constructor Injection

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.


Why Constructor Injection Is Important

Constructor injection offers several advantages.

Dependencies Are Explicit

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.

Easier Testing

You can provide fake implementations during tests.

val repository = UserRepository(
    fakeApi
)

Better Separation of Responsibilities

The class focuses on its job rather than figuring out how all of its dependencies should be constructed.


Step 4: Creating Hilt Modules

Constructor injection does not work for every dependency.

You may need to provide objects from:

  • Third-party libraries
  • Interfaces
  • Retrofit
  • OkHttp
  • Room
  • External SDKs
  • Classes whose constructors cannot be modified

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 @Binds

Two important Hilt annotations are @Provides and @Binds.

@Provides

Use @Provides when you need to manually construct an object.

For example:

@Provides
fun provideRetrofit(): Retrofit {
    return Retrofit.Builder()
        .baseUrl(BASE_URL)
        .build()
}

@Binds

Use @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.


Understanding Hilt Components and Scopes

One of Hilt’s most powerful features is its predefined component hierarchy.

Hilt provides components associated with Android application lifecycles.

Important components include:

  • SingletonComponent
  • ActivityRetainedComponent
  • ViewModelComponent
  • ActivityComponent
  • FragmentComponent
  • ViewComponent
  • ViewWithFragmentComponent
  • ServiceComponent

Each 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 and ViewModel

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.


Hilt with Retrofit

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 with Room Database

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.


Qualifiers in Hilt

Sometimes an application needs multiple objects of the same type.

For example, you might have two Retrofit instances:

  • Public API
  • Authentication API

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.


Testing Hilt Dependencies

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:

  • APIs
  • Databases
  • Authentication services
  • File systems
  • Analytics services
  • Remote configuration

Common Hilt Mistakes

Hilt is powerful, but it does not automatically create good architecture.

Creating One Huge Module

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

Using Singleton Everywhere

Not every dependency needs to be a singleton.

Long-lived objects can consume resources unnecessarily.

Choose the smallest appropriate lifecycle scope.

Injecting Too Much

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.

Hiding Dependencies

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.


Hilt Best Practices for Modern Android Apps

For a scalable project, consider these recommendations:

Prefer Constructor Injection

Use constructor injection whenever possible.

Use Modules for External Dependencies

Use @Provides and @Binds when constructor injection is not appropriate.

Keep Modules Focused

Group related dependencies together.

Use Interfaces for Abstractions

Repositories and services can depend on interfaces when multiple implementations may be needed.

Scope Dependencies Carefully

Match the dependency’s lifetime to its actual requirements.

Avoid Unnecessary Singletons

A singleton is not automatically better or faster.

Keep ViewModels Focused

Hilt can inject dependencies into ViewModels, but the ViewModel should still have a clear responsibility.

Design for Testing

Make it easy to replace real services with fake or test implementations.

Keep Business Logic Independent

Dependency injection should support good architecture rather than replace it.


A Recommended Hilt Architecture

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.


When Should You Use Hilt?

Hilt is particularly useful when:

  • The application contains many dependencies.
  • Multiple layers communicate with one another.
  • The project needs extensive automated testing.
  • ViewModels depend on repositories.
  • Repositories depend on APIs or databases.
  • Multiple implementations of interfaces exist.
  • The application is expected to grow over time.

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.


Conclusion

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.

Trusted Sources and Further Reading

Leave a Reply

Your email address will not be published. Required fields are marked *

Solverwp- WordPress Theme and Plugin