Skip to main content
The Space Flight News app uses Dagger Hilt for dependency injection, a compile-time DI framework that generates code to wire dependencies automatically.

Why Dependency Injection?

Dependency Injection provides several benefits:

Testability

Easy to swap real implementations with mocks for testing

Decoupling

Classes don’t create their dependencies, making them more flexible

Reusability

Dependencies can be shared across multiple classes

Maintainability

Changing implementations doesn’t require modifying dependent classes

Hilt Setup

Application Class

The app is annotated with @HiltAndroidApp to enable dependency injection:
com/bsvillarraga/spaceflightnews/MyApp.kt
package com.bsvillarraga.spaceflightnews

import android.app.Application
import dagger.hilt.android.HiltAndroidApp

@HiltAndroidApp
class MyApp: Application()
This annotation triggers Hilt’s code generation and initializes the dependency graph when the app starts.

Modules

Hilt uses modules to define how to provide dependencies. The app has two main modules:
  1. NetworkModule - Provides network-related dependencies
  2. RoomModule - Provides database-related dependencies

NetworkModule

This module provides all network and repository dependencies:
com/bsvillarraga/spaceflightnews/di/NetworkModule.kt
@Module
@InstallIn(SingletonComponent::class)
object NetworkModule {
    @Singleton
    @Provides
    fun provideRetrofit(): Retrofit {
        val client = OkHttpClient.Builder()
            .addInterceptor { chain ->
                val original = chain.request()
                val originalUrl = original.url()

                val newUrl = originalUrl.newBuilder()
                    .addQueryParameter("format", "json")
                    .build()

                val newRequest = original.newBuilder()
                    .url(newUrl)
                    .build()

                chain.proceed(newRequest)
            }
            .build()

        return Retrofit.Builder()
            .baseUrl("https://api.spaceflightnewsapi.net/")
            .client(client)
            .addConverterFactory(GsonConverterFactory.create())
            .build()
    }

    @Provides
    @Singleton
    fun provideNetworkHelper(@ApplicationContext context: Context): NetworkHelper {
        return NetworkHelper(context)
    }

    @Provides
    @Singleton
    fun provideApiHelper(networkHelper: NetworkHelper): ApiHelper {
        return ApiHelper(networkHelper)
    }

    @Provides
    @Singleton
    fun providePaginationManager(paginationDao: PaginationDao): PaginationManager {
        return PaginationManager(paginationDao)
    }

    @Singleton
    @Provides
    fun provideArticlesApiClient(retrofit: Retrofit): ArticlesApiClient =
        retrofit.create(ArticlesApiClient::class.java)

    @Provides
    @Singleton
    fun provideArticleRepository(
        api: ArticlesApiClient,
        apiHelper: ApiHelper,
        paginationManager: PaginationManager
    ): ArticleRepository =
        ArticleRepositoryImpl(api, apiHelper, paginationManager)
}

Key Annotations

Marks a class as a Hilt module. Modules tell Hilt how to provide instances of certain types.
Specifies which Hilt component this module should be installed in. SingletonComponent means these dependencies live as long as the application.Other options include:
  • ActivityComponent - Scoped to an Activity’s lifecycle
  • FragmentComponent - Scoped to a Fragment’s lifecycle
  • ViewModelComponent - Scoped to a ViewModel’s lifecycle
Marks a function that provides a dependency. The return type tells Hilt what this function provides.
Ensures only one instance of this dependency exists throughout the app’s lifetime.
Qualifier annotation that tells Hilt to inject the Application context, not an Activity context.

Dependency Chain in NetworkModule

Notice how dependencies reference each other:
Hilt automatically resolves the dependency graph. When you request ArticleRepository, Hilt knows it needs ArticlesApiClient, ApiHelper, and PaginationManager, and provides them automatically.

Retrofit Configuration

The Retrofit provider includes a custom interceptor:
com/bsvillarraga/spaceflightnews/di/NetworkModule.kt:33-48
val client = OkHttpClient.Builder()
    .addInterceptor { chain ->
        val original = chain.request()
        val originalUrl = original.url()

        val newUrl = originalUrl.newBuilder()
            .addQueryParameter("format", "json")
            .build()

        val newRequest = original.newBuilder()
            .url(newUrl)
            .build()

        chain.proceed(newRequest)
    }
    .build()
This interceptor adds ?format=json to every API request automatically. This is preferable to adding it manually to each endpoint.

Repository Binding

The repository provider binds the interface to its implementation:
com/bsvillarraga/spaceflightnews/di/NetworkModule.kt:84-91
@Provides
@Singleton
fun provideArticleRepository(
    api: ArticlesApiClient,
    apiHelper: ApiHelper,
    paginationManager: PaginationManager
): ArticleRepository =
    ArticleRepositoryImpl(api, apiHelper, paginationManager)
The return type is ArticleRepository (interface), but the implementation is ArticleRepositoryImpl. This allows the domain and presentation layers to depend on the interface only.

RoomModule

This module provides database dependencies:
com/bsvillarraga/spaceflightnews/di/RoomModule.kt
@Module
@InstallIn(SingletonComponent::class)
object RoomModule {
    private const val SPACE_FLIGHT_NEW = "space_flight_news_database"

    @Singleton
    @Provides
    fun provideAppDatabase(@ApplicationContext context: Context) =
        Room.databaseBuilder(context, SpaceFlightNewsDb::class.java, SPACE_FLIGHT_NEW).build()

    @Singleton
    @Provides
    fun providePaginationDao(db: SpaceFlightNewsDb) = db.paginationDao()
}
Hilt provides the Room database instance as a singleton, ensuring only one database connection exists.
@Singleton
@Provides
fun provideAppDatabase(@ApplicationContext context: Context) =
    Room.databaseBuilder(context, SpaceFlightNewsDb::class.java, SPACE_FLIGHT_NEW).build()
The DAO provider depends on the database. Hilt automatically provides the database instance.
@Singleton
@Provides
fun providePaginationDao(db: SpaceFlightNewsDb) = db.paginationDao()

Constructor Injection

Classes that Hilt can create automatically use @Inject on their constructor:

Repository Implementation

com/bsvillarraga/spaceflightnews/data/repository/ArticleRepositoryImpl.kt:29-33
class ArticleRepositoryImpl @Inject constructor(
    private val api: ArticlesApiClient,
    private val apiHelper: ApiHelper,
    private val paginationManager: PaginationManager
) : ArticleRepository {
The @Inject annotation tells Hilt this class can be constructed by providing these three dependencies. No need for a @Provides function in a module.

Use Cases

com/bsvillarraga/spaceflightnews/domain/usecase/GetArticlesUseCase.kt:15-17
class GetArticlesUseCase @Inject constructor(
    private val repository: ArticleRepository
) {
Use cases are also injected via constructor, receiving the ArticleRepository interface.

ViewModels

ViewModels use a special annotation:
com/bsvillarraga/spaceflightnews/presentation/ui/articles/viewmodel/ArticlesViewModel.kt:20-24
@HiltViewModel
class ArticlesViewModel @Inject constructor(
    private val articleUseCase: GetArticlesUseCase
) : ViewModel() {
ViewModels require @HiltViewModel annotation instead of just @Inject. This integrates with Android’s ViewModel lifecycle.

Injection Points

Fragments

Fragments use @AndroidEntryPoint to enable field injection:
com/bsvillarraga/spaceflightnews/presentation/ui/articles/ArticlesFragment.kt:41-49
@AndroidEntryPoint
class ArticlesFragment : Fragment(), MenuProvider {
    private lateinit var binding: FragmentArticlesBinding
    private lateinit var adapter: ArticleAdapter

    private var searchView: SearchView? = null
    private var searchMenuItem: MenuItem? = null

    private val viewModel: ArticlesViewModel by viewModels()
The by viewModels() delegate automatically obtains the ViewModel from Hilt with all its dependencies injected.

Activities

Activities also use @AndroidEntryPoint:
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
    // Hilt can inject dependencies here
}

Dependency Graph Visualization

Here’s how the complete dependency graph flows:
Application (@HiltAndroidApp)

    ├─> NetworkModule (@InstallIn(SingletonComponent))
    │   ├─> Retrofit (@Singleton)
    │   ├─> NetworkHelper (@Singleton)
    │   ├─> ApiHelper (@Singleton)
    │   ├─> PaginationManager (@Singleton)
    │   ├─> ArticlesApiClient (@Singleton)
    │   └─> ArticleRepository (@Singleton)
    │       └─> ArticleRepositoryImpl (@Inject)

    ├─> RoomModule (@InstallIn(SingletonComponent))
    │   ├─> SpaceFlightNewsDb (@Singleton)
    │   └─> PaginationDao (@Singleton)

    ├─> Use Cases (@Inject)
    │   └─> GetArticlesUseCase

    ├─> ViewModels (@HiltViewModel)
    │   └─> ArticlesViewModel

    └─> UI Components (@AndroidEntryPoint)
        └─> ArticlesFragment

Benefits in Practice

Without DI, you’d need code like:
// DON'T DO THIS - No DI
val retrofit = Retrofit.Builder()...
val api = retrofit.create(ArticlesApiClient::class.java)
val networkHelper = NetworkHelper(context)
val apiHelper = ApiHelper(networkHelper)
val paginationDao = database.paginationDao()
val paginationManager = PaginationManager(paginationDao)
val repository = ArticleRepositoryImpl(api, apiHelper, paginationManager)
val useCase = GetArticlesUseCase(repository)
val viewModel = ArticlesViewModel(useCase)
With Hilt, it’s just:
// DO THIS - With Hilt
private val viewModel: ArticlesViewModel by viewModels()
In tests, you can provide mock implementations:
@TestInstallIn(
    components = [SingletonComponent::class],
    replaces = [NetworkModule::class]
)
@Module
object TestNetworkModule {
    @Provides
    @Singleton
    fun provideArticleRepository(): ArticleRepository {
        return FakeArticleRepository() // Mock implementation
    }
}
Hilt automatically handles lifecycle:
  • Singletons live for the entire app
  • Activity-scoped dependencies die with the Activity
  • ViewModel-scoped dependencies survive configuration changes
Hilt generates code at compile time. If dependencies are missing or circular, you get a compile error, not a runtime crash.

Common Patterns

Singleton Dependencies

Use @Singleton for dependencies that should be shared:
@Provides
@Singleton
fun provideRetrofit(): Retrofit { /* ... */ }

Good for Singletons

  • Network clients (Retrofit, OkHttpClient)
  • Database instances (Room)
  • Repositories
  • SharedPreferences
  • Analytics/Logging services

Unscoped Dependencies

Omit @Singleton for dependencies that should be created fresh:
@Provides
fun provideMapper(): DataMapper {
    return DataMapper()
}

Don't Use Singletons For

  • Stateful objects that shouldn’t be shared
  • Objects with short lifecycles
  • Test doubles in unit tests

Interface Binding

Always return interfaces when possible:
@Provides
@Singleton
fun provideArticleRepository(
    // ... dependencies
): ArticleRepository =  // Return interface type
    ArticleRepositoryImpl(...)  // Actual implementation
This allows you to swap implementations without changing dependent classes.

Debugging Tips

1

Check Module Installation

Ensure modules are installed in the correct component with @InstallIn
2

Verify @Inject Annotations

Constructor injection requires @Inject on the constructor
3

Check Return Types

@Provides functions should return the type you want to inject (usually interfaces)
4

Look for Circular Dependencies

If A depends on B and B depends on A, Hilt can’t create either
5

Rebuild the Project

Hilt generates code at compile time - clean and rebuild if things seem broken

Architecture Overview

See how DI fits into the overall architecture

Clean Architecture

Understand the layers that DI connects

Build docs developers (and LLMs) love