Module 12 of 20

Dependency Injection (Hilt & Dagger Deep Dive)

Implement scalable dependency injection in Android applications using Dagger 2 and Jetpack Hilt.

Module 12: Dependency Injection (Hilt & Dagger Deep Dive)

Learning Objectives

By the end of this module, you’ll understand:

  • What a dependency really is
  • Tight coupling vs loose coupling
  • Inversion of Control (IoC)
  • Dependency Injection
  • Constructor Injection
  • Field Injection
  • Method Injection
  • Service Locator vs DI
  • Why Dagger exists
  • Compile-time dependency graphs
  • Hilt architecture
  • Components
  • Scopes
  • Modules
  • Providers
  • Bindings
  • Qualifiers
  • Entry Points
  • Assisted Injection
  • Best practices

Part 1 — What Is a Dependency?

Suppose we have:

class LoginRepository(
    private val api: ApiService
)

Question:

What is the dependency?

Answer:

ApiService

Because the repository depends on it.

General definition:

A dependency is anything another class requires in order to perform its work.

Examples:

Repository



Retrofit



Room



SharedPreferences



Logger



Firebase

2. Object Creation Problem

Imagine:

class LoginViewModel {

    val repository = LoginRepository()

}

Seems harmless.

But then:

LoginRepository



Retrofit



OkHttp



Logger



Database



Preferences

Who creates all of these?

Soon:

ViewModel



Repository



Retrofit



OkHttp



CertificatePinner



Cache



Database



DAO

The ViewModel is now responsible for building the entire object graph.

That’s a design smell.


3. Tight Coupling

Suppose:

val repository = LoginRepository()

Now the ViewModel knows:

  • Which implementation to use
  • How to construct it
  • When to construct it

They’re tightly coupled.

Imagine changing:

Retrofit



Ktor

Every place that constructs the repository may need updates.

Testing becomes difficult too.


4. Loose Coupling

Instead:

ViewModel



Repository Interface



Implementation

The ViewModel depends only on an abstraction.

It doesn’t care whether the implementation uses:

  • Retrofit
  • Mock data
  • Room
  • GraphQL

This follows the Dependency Inversion Principle from Module 10.


Part 2 — Inversion of Control (IoC)

Without IoC:

ViewModel



Creates Repository

With IoC:

ViewModel



Receives Repository

Notice the difference.

The ViewModel no longer controls object creation.

Something else does.

That “something else” is often a DI container.


5. Dependency Injection

Dependency Injection means:

Instead of creating dependencies yourself, they are provided from outside.

Example:

Instead of:

val repository = LoginRepository()

Use:

class LoginViewModel(
    private val repository: LoginRepository
)

Now the ViewModel simply says:

“I need a LoginRepository.”

It doesn’t say:

“I’ll build one.”

That’s a huge architectural improvement.


Part 3 — Types of Injection


Constructor Injection (Preferred)

class UserRepository(
    private val api: ApiService
)

Advantages:

  • Immutable dependencies
  • Easy testing
  • Dependencies are obvious
  • Works well with Hilt

This is Google’s preferred approach.


Field Injection

@Inject
lateinit var repository: UserRepository

Useful in Android framework classes like:

  • Activity
  • Fragment
  • Service

Less ideal for regular Kotlin classes because the dependency isn’t obvious from the constructor.


Method Injection

fun initialize(
    logger: Logger
)

Used less frequently.

Helpful when a dependency is only needed for a specific operation.


Part 4 — Service Locator vs DI

Some older codebases use a Service Locator.

ViewModel



ServiceLocator



Repository

The ViewModel asks for dependencies.

With DI:

DI Container



ViewModel



Repository

The container provides them automatically.

DI makes dependencies more explicit and improves testability.


Part 5 — Why Dagger Exists

Imagine:

100 classes.

Each needs:

  • Repository
  • API
  • Database
  • Logger
  • Analytics
  • Preferences

Manually wiring everything becomes tedious.

MainActivity



ViewModel



Repository



Api



Client



Cache



Database

Someone has to build this graph.

Dagger automates it.


6. Dependency Graph

Imagine:

LoginViewModel



LoginRepository



ApiService



Retrofit



OkHttpClient

This is a dependency graph.

Dagger generates code to build it.

Not at runtime.

At compile time.

That’s one reason Dagger is fast.


Part 6 — Hilt

Dagger is powerful.

But historically it required lots of setup.

Hilt is Google’s opinionated layer on top of Dagger.

Think:

Dagger



Hilt

Hilt removes much of the boilerplate while still using Dagger under the hood.


7. Hilt Flow

Imagine:

Activity



ViewModel



Repository



Api

Without Hilt:

You manually build everything.

With Hilt:

Activity



@Inject



Automatically Provided

The dependency graph is generated for you.


Part 7 — Components

One of the hardest Hilt concepts.

A component is a container that owns objects for a certain lifetime.

Imagine a hierarchy:

SingletonComponent


ActivityRetainedComponent


ViewModelComponent


ActivityComponent


FragmentComponent


ViewComponent

Each component lives for a different duration.


SingletonComponent

Lives as long as the application.

Perfect for:

  • Retrofit
  • Room Database
  • Analytics
  • SharedPreferences
  • Repositories (when appropriate)

Only one instance exists.


ActivityComponent

Lives as long as one Activity instance.

Destroyed when the Activity is destroyed.

Useful for Activity-scoped dependencies.


ViewModelComponent

Lives as long as the ViewModel.

Perfect for:

  • Use Cases
  • Screen-specific state holders
  • Screen repositories if appropriate

FragmentComponent

Lives with a Fragment.

Destroyed when the Fragment is destroyed.


Why Scopes Matter

Suppose Retrofit is unscoped.

Every injection creates:

Retrofit



New OkHttp



New Cache

Wasteful.

Instead:

Singleton



One Retrofit



Shared Everywhere

Efficient.


Part 8 — Modules

Some classes cannot use constructor injection.

Example:

Retrofit.

You don’t own its constructor configuration.

Instead:

@Module

tells Hilt:

“Here’s how to build this dependency.”

Example (conceptually):

@Module
object NetworkModule {

    @Provides
    fun provideRetrofit(): Retrofit

}

@Provides

Use when:

You must execute code to create the object.

Example:

Retrofit.Builder()



baseUrl()



converterFactory()



build()

You can’t annotate Retrofit’s constructor.

So you provide it.


@Binds

Suppose:

UserRepository



UserRepositoryImpl

Hilt needs to know:

Which implementation satisfies the interface?

Use:

@Binds

It maps:

Interface



Implementation

without manually creating the object.


@Inject Constructor

If you own the class:

class UserRepository @Inject constructor(
    private val api: ApiService
)

No module required.

Hilt can construct it automatically.

Rule of thumb:

  • Own the class? → Prefer @Inject constructor.
  • Don’t own the class? → Use @Provides.

Part 9 — Qualifiers

Suppose:

Two Retrofit instances.

Public API

Private API

Both have type:

Retrofit

How does Hilt know which one you want?

Use qualifiers.

Example:

@PublicApi

@PrivateApi

Now injections are unambiguous.


Part 10 — Assisted Injection

Sometimes not every parameter comes from DI.

Example:

Repository



Injected

ProductId



Runtime Value

The runtime value isn’t known beforehand.

Assisted injection allows mixing injected dependencies with runtime parameters.

You’ll commonly encounter this with ViewModels that need runtime arguments or factories for complex objects.


Part 11 — Entry Points

Most Android classes support Hilt directly.

Some don’t.

Example:

  • ContentProvider
  • Some third-party callbacks

Entry Points allow those classes to access the dependency graph.

They’re an advanced feature used when normal injection isn’t available.


Part 12 — Hilt in a Modern App

Typical dependency graph:

App


SingletonComponent

 ├── Retrofit
 ├── OkHttpClient
 ├── RoomDatabase
 ├── Preferences
 └── UserRepository


LoginViewModel


LoginScreen

Notice the direction:

Dependencies flow downward.

No class manually creates the objects it depends on.


Real-World Example

Imagine a weather app.

WeatherScreen


WeatherViewModel


WeatherRepository
      ┌─┴───────────┐
      ▼             ▼
WeatherApi     WeatherDatabase
      │             │
      └──────┬──────┘

        Weather Data

With Hilt:

  • WeatherApi is provided by a NetworkModule.
  • WeatherDatabase is provided by a DatabaseModule.
  • WeatherRepository uses constructor injection.
  • WeatherViewModel receives the repository automatically.
  • WeatherScreen obtains the ViewModel using Hilt integration.

No manual wiring.


Common Mistakes

❌ Creating dependencies inside ViewModels

val repository = UserRepository()

This defeats DI.


❌ Making everything a Singleton

Singletons should represent application-wide state or expensive shared resources.

Don’t use them indiscriminately.


❌ Using field injection everywhere

Prefer constructor injection whenever possible.


❌ Putting business logic inside DI modules

Modules should construct objects, not implement business rules.


❌ Ignoring interfaces

Depending directly on concrete implementations reduces flexibility and makes testing harder.


Mental Model

Think of a movie production.

Without DI:

Actor



Builds Camera



Builds Lights



Builds Microphone



Acts

Ridiculous.

With DI:

Production Team



Provides Camera



Provides Lights



Provides Microphone



Actor Acts

The actor focuses on acting.

The production team provides the tools.

Your ViewModel should be the actor—not the equipment manager.


Best Practices

  • Prefer constructor injection.
  • Keep dependencies immutable (val).
  • Use interfaces for abstractions.
  • Scope expensive objects appropriately.
  • Use @Inject constructors whenever you own the class.
  • Use @Provides for third-party libraries.
  • Use @Binds for interface-to-implementation mappings.
  • Keep DI modules focused solely on object creation.
  • Design dependencies as a directed graph, not a web of cyclic references.

Interview Questions

  1. What is a dependency?
  2. What problems does Dependency Injection solve?
  3. Explain Inversion of Control.
  4. Compare constructor, field, and method injection.
  5. Why is constructor injection generally preferred?
  6. What is the difference between Dagger and Hilt?
  7. What is a dependency graph?
  8. Explain the purpose of Hilt Components and Scopes.
  9. When should you use @Inject, @Provides, and @Binds?
  10. What problem do qualifiers solve?

Module 12 Summary

You now understand the object wiring strategy behind modern Android applications:

  • Dependencies are collaborators that a class needs.
  • Dependency Injection separates object creation from object usage.
  • Inversion of Control shifts construction responsibility to a container.
  • Constructor injection is the preferred pattern.
  • Dagger generates dependency graphs at compile time.
  • Hilt simplifies Dagger integration with Android lifecycles.
  • Scopes control object lifetimes.
  • Modules define how to create objects you don’t own.
  • Qualifiers resolve ambiguity between multiple implementations.

With Modules 10 (Architecture), 11 (Concurrency), and 12 (Dependency Injection) completed, you now understand the three pillars of a modern Android codebase:

  • How it’s organized (MVVM & Clean Architecture).
  • How work executes (Coroutines & Flow).
  • How objects are created and connected (Hilt & Dagger).

Next Module: Networking (Retrofit, OkHttp, REST APIs & Serialization)

In Module 13, we’ll trace a network request from the moment the user taps a button all the way to receiving JSON from a remote server.

We’ll cover:

  • The HTTP protocol (requests, responses, methods, status codes).
  • REST APIs and common endpoint design.
  • JSON serialization/deserialization.
  • Retrofit architecture and internals.
  • OkHttp interceptors, authenticators, and connection pooling.
  • Error handling and retry strategies.
  • Authentication (Bearer tokens, OAuth concepts).
  • Logging, timeouts, and caching.
  • Best practices for production networking.

This module will connect everything you’ve learned so far—ViewModel, Coroutines, Flow, Repositories, and DI—into a complete end-to-end data flow from the UI to a backend service.