Skip to content

Intermediate: Android basics · Navigation and app architecture · topic 30 of 50 · 30 min

Dependency injection with Hilt

Explanation

Dependency injection (DI) means a class receives the objects it needs (its dependencies) from outside instead of creating them itself. This makes classes easy to test with fakes and easy to reconfigure. Hilt is the DI library recommended for Android: annotate the Application with @HiltAndroidApp, activities with @AndroidEntryPoint, ViewModels with @HiltViewModel and @Inject constructor, and provide objects in @Module classes.

The problem

A ViewModel needs a repository; the repository needs an API client and a database; the API client needs an HTTP client with settings. If each class creates its own dependencies with constructors inside, everything is tightly glued together: you cannot test the ViewModel with a fake repository, and the app may accidentally create three database instances.

The idea: pass dependencies in

kotlin
// Hard to test: creates its own dependency
class OrdersViewModelBad : ViewModel() {
    private val repo = OrderRepository(RealApi(), RealDatabase())
}

// Easy to test: receives it (constructor injection)
class OrdersViewModel(private val repo: OrderRepository) : ViewModel()
In a test, pass a fake repository with ready-made data.

This is dependency injection: dependencies are injected (given) from outside. You can do it by hand, by creating objects in one place (an AppContainer class) and passing them down. That is perfectly fine for small apps. As apps grow, a library like Hilt writes this wiring code for you.

Hilt setup in brief

kotlin
// 1. The Application class
@HiltAndroidApp
class ShopApp : Application()

// 2. Activities that need injection
@AndroidEntryPoint
class MainActivity : ComponentActivity() { /* ... */ }

// 3. ViewModels
@HiltViewModel
class OrdersViewModel @Inject constructor(
    private val repo: OrderRepository,
) : ViewModel()

// 4. Classes Hilt can build itself
class OrderRepository @Inject constructor(
    private val api: OrderApi,
    private val dao: OrderDao,
)
Hilt also needs its Gradle plugin and KSP set up; follow the current Hilt guide for exact versions.

An @Inject constructor tells Hilt how to create a class: by calling that constructor with the dependencies it needs. In Compose, get a Hilt ViewModel with hiltViewModel() instead of viewModel(). Register the Application class in the manifest with android:name=".ShopApp".

Modules for things you cannot annotate

Some objects come from libraries (Retrofit, Room), or are interfaces. You cannot add @Inject to their constructors, so you tell Hilt how to provide them in a module: an object with @Module and @InstallIn, containing @Provides functions (or @Binds for interfaces).

kotlin
@Module
@InstallIn(SingletonComponent::class)        // lives as long as the app
object NetworkModule {
    @Provides
    @Singleton                               // one instance for the whole app
    fun provideOrderApi(): OrderApi =
        Retrofit.Builder()
            .baseUrl("https://api.example.com/")
            .addConverterFactory(/* JSON converter */)
            .build()
            .create(OrderApi::class.java)
}
@Singleton with SingletonComponent means the same OrderApi is reused everywhere.
Think of it like this: Without DI, every employee buys their own laptop, chair and internet connection on day one. With DI, the office manager (Hilt) prepares everything and hands each person exactly what they need. For a test day, the manager can hand out practice laptops (fakes) instead, and nobody has to change how they work.

Hilt checks the whole dependency graph at compile time. If something is missing, for example a repository needs an OrderApi that no module provides, the build fails with an error naming the missing type, instead of the app crashing later on a user's phone.

When to use Hilt

  • Small learning app or a few screens: manual DI with a simple container is enough.
  • Medium to large app, several developers, many repositories: Hilt saves a lot of wiring code and mistakes.
  • Job interviews: Hilt (and the idea of DI) is commonly asked about in Android roles.

Common mistakes

  • Forgetting @HiltAndroidApp or the manifest android:name entry: the app crashes at start.
  • Forgetting @AndroidEntryPoint on the activity that hosts Hilt ViewModels.
  • Using viewModel() instead of hiltViewModel() for a @HiltViewModel.
  • Making everything @Singleton. Only share objects that really should be app-wide.

Need help?

Stuck on a word or an idea? Search this course, or every course, and jump straight to the explanation or a worked question.

Try:

Watch the lesson

Dependency injection with Hilt · 1/6

Pass dependencies in

Dependency injection means a class receives what it needs from outside, instead of creating it.

Prefer a video by a teacher? Find free videos on “hilt dependency injection android tutorial hiltviewmodel” on YouTube ↗

In real life: Swapping a fake payment gateway in tests

A ticket-booking app's CheckoutViewModel depends on a PaymentGateway interface. In production, Hilt binds it to the real gateway client. In tests, the team replaces the binding with a FakePaymentGateway that always succeeds or always fails, so they can test every checkout path in seconds without real money or network.

kotlin
interface PaymentGateway { suspend fun pay(paise: Long): Boolean }

class RealGateway @Inject constructor() : PaymentGateway {
    override suspend fun pay(paise: Long) = true   // would call the bank
}

@Module
@InstallIn(SingletonComponent::class)
abstract class PaymentModule {
    @Binds abstract fun bindGateway(impl: RealGateway): PaymentGateway
}

@HiltViewModel
class CheckoutViewModel @Inject constructor(private val gateway: PaymentGateway) : ViewModel()
@Binds tells Hilt which implementation to use for an interface.

Practice (0/3 done)

Task 1 · Question

What is the main benefit of constructor injection?

Task 2 · Question

How do you tell Hilt which class implements the interface PaymentGateway?

Task 3 · Practice

Take the CartViewModel from topic 2 and refactor it to receive a CartRepository through its constructor. First do manual DI: create the repository in an AppContainer and pass it via a ViewModel factory or by hand. Then (optional) add Hilt: @HiltAndroidApp, @AndroidEntryPoint, @HiltViewModel with @Inject constructor and hiltViewModel().

Quick quiz

Question 1

1.Which annotation goes on the Application class?

Question 2

2.What does @Inject constructor do?

Question 3

3.How do you provide a Retrofit API object with Hilt?

Question 4

4.Is Hilt required for small apps?

Read more (free)

Open a question to see the answer and why. The first ones are this topic's own quiz, so try the quiz above before you peek.

Which annotation goes on the Application class?

Answer: @HiltAndroidApp

It triggers Hilt's code generation for the app.

What does @Inject constructor do?

Answer: Tells Hilt how to create the class

Hilt calls it with the needed dependencies.

How do you provide a Retrofit API object with Hilt?

Answer: A @Provides function in a @Module

Library objects are provided in modules.

Is Hilt required for small apps?

Answer: No, manual DI is fine for small apps

Use Hilt when the wiring grows.

Which injection style is recommended?Related

Answer: Constructor injection

Constructor injection gives final fields and easy tests.

From Spring and dependency injection

What is dependency injection?Related

Answer: Giving a class its dependencies from outside instead of it creating them

Dependencies are supplied, usually via the constructor.

From Dependency injection and service lifetimes

What does dependency injection mean in practice?Related

Answer: Passing dependencies in as parameters instead of using globals

It is mostly "use parameters for the things you depend on".

From Test doubles and testable design

Which design makes test doubles easy to use?Related

Answer: Depending on interfaces passed in through the constructor

Constructor injection of interfaces.

From Test doubles: fakes, stubs and mocks

Pass the quiz (60% or more) or mark this topic as done when you are ready.

Your progress is saved on this device only.

Next steps

  1. This is the last topic of “Navigation and app architecture”. The module exam comes next.
  2. Module 7 exam: Navigation and app architecture · 13 questions · 25 min, without help. 4 topics of this module still to finish first.
  3. Then the next module, Saving data and calling APIs. 50 topics left in the course before the final exam.

← Previous: App architecture: layers and repositories