Skip to main content
The Compose Project Template uses Hilt (Dagger for Android) for dependency injection. Hilt automates dependency management, reduces boilerplate, and ensures proper scoping of dependencies.

Hilt setup

Hilt is initialized at the application level with a single annotation.

Application class

The @HiltAndroidApp annotation triggers Hilt’s code generation:
package es.mobiledev.cpt

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

@HiltAndroidApp
class App : Application() {
    override fun onCreate() {
        super.onCreate()
    }
}
The @HiltAndroidApp annotation generates a base class that serves as the application-level dependency container.

Gradle configuration

Hilt is configured in modules that need dependency injection:
plugins {
    alias(libs.plugins.android.library)
    alias(libs.plugins.kotlin.android)
    alias(libs.plugins.ksp)           // For annotation processing
    alias(libs.plugins.hilt)          // Hilt plugin
}

dependencies {
    implementation(libs.hilt.android)
    implementation(libs.hilt.navigation)  // For Compose navigation
    ksp(libs.hilt.compiler)               // Annotation processor
}
The project uses KSP (Kotlin Symbol Processing) instead of KAPT for faster compilation.

Component hierarchy

Hilt provides several predefined components with different lifecycles:

SingletonComponent

Lives for the entire application lifetime. Used for repositories, data sources, and network clients.

ActivityRetainedComponent

Survives configuration changes but tied to Activity lifecycle.

ViewModelComponent

Scoped to ViewModel lifecycle. Automatically used for ViewModels.

ActivityComponent

Scoped to Activity lifecycle.
Most modules in this project use SingletonComponent to ensure single instances of repositories and data sources.

DI modules structure

The project organizes Hilt modules by layer and functionality.

Domain layer: UseCaseModule

Provides use case implementations:
package es.mobiledev.domain.usecase.di

import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import es.mobiledev.domain.gateway.article.ArticleGateway
import es.mobiledev.domain.gateway.preferences.PreferencesGateway
import es.mobiledev.domain.usecase.article.*
import es.mobiledev.domain.usecase.preferences.*

@Module
@InstallIn(SingletonComponent::class)
object UseCaseModule {
    @Provides
    fun getArticlesUseCaseProvider(
        articleGateway: ArticleGateway
    ) = GetArticlesUseCaseImpl(articleGateway) as GetArticlesUseCase
    
    @Provides
    fun getArticleByIdUseCaseProvider(
        articleGateway: ArticleGateway
    ) = GetArticleByIdUseCaseImpl(articleGateway) as GetArticleByIdUseCase
    
    @Provides
    fun getFavoritesArticlesUseCaseProvider(
        articleGateway: ArticleGateway
    ) = GetFavoriteArticlesUseCaseImpl(articleGateway) as GetFavoriteArticlesUseCase
    
    @Provides
    fun isArticleFavoriteUseCaseProvider(
        articleGateway: ArticleGateway
    ) = IsArticleFavoriteUseCaseImpl(articleGateway) as IsArticleFavoriteUseCase
    
    @Provides
    fun saveOrRemoveFavoriteArticleUseCaseProvider(
        articleGateway: ArticleGateway
    ) = SaveOrRemoveFavoriteArticleUseCaseImpl(articleGateway) as SaveOrRemoveFavoriteArticleUseCase
    
    @Provides
    fun saveLastOpenTimeUseCaseProvider(
        prefersGateway: PreferencesGateway
    ) = SaveLastOpenTimeUseCaseImpl(prefersGateway) as SaveLastOpenTimeUseCase
    
    @Provides
    fun getLastOpenTimeUseCaseProvider(
        prefersGateway: PreferencesGateway
    ) = GetLastOpenTimeUseCaseImpl(prefersGateway) as GetLastOpenTimeUseCase
}
Each use case is provided as:
  1. Accept gateway interface as parameter (Hilt injects it)
  2. Create implementation instance
  3. Return as interface type

Data layer: RepositoryModule

Provides repository implementations that implement gateway interfaces:
package es.mobiledev.data.repository.di

import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import es.mobiledev.data.repository.article.ArticleRepository
import es.mobiledev.data.repository.preferences.PreferencesRepository
import es.mobiledev.data.source.article.ArticleLocalDataSource
import es.mobiledev.data.source.article.ArticleRemoteDataSource
import es.mobiledev.data.source.preferences.PreferencesDataSource
import es.mobiledev.domain.gateway.article.ArticleGateway
import es.mobiledev.domain.gateway.preferences.PreferencesGateway

@Module
@InstallIn(SingletonComponent::class)
object RepositoryModule {
    @Provides
    fun preferencesRepositoryProvider(
        preferencesDataSource: PreferencesDataSource
    ) = PreferencesRepository(
        preferences = preferencesDataSource
    ) as PreferencesGateway
    
    @Provides
    fun articleRepositoryProvider(
        articleRemoteDataSource: ArticleRemoteDataSource,
        articleLocalDataSource: ArticleLocalDataSource
    ) = ArticleRepository(
        remote = articleRemoteDataSource,
        local = articleLocalDataSource
    ) as ArticleGateway
}
Repositories implement gateway interfaces defined in the domain layer, enabling dependency inversion.

Data layer: RemoteModule

Provides networking dependencies and remote data sources:
package es.mobiledev.data.remote.di

import com.squareup.moshi.Moshi
import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import es.mobiledev.data.remote.article.ArticleRemoteDataSourceImpl
import es.mobiledev.data.remote.article.ArticleWs
import es.mobiledev.data.remote.util.BASE_URL
import es.mobiledev.data.source.article.ArticleRemoteDataSource
import okhttp3.Interceptor
import okhttp3.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor
import retrofit2.Retrofit
import retrofit2.converter.moshi.MoshiConverterFactory

@Module
@InstallIn(SingletonComponent::class)
object RemoteModule {
    @Provides
    fun interceptorProvider(): Interceptor =
        HttpLoggingInterceptor().apply {
            level = HttpLoggingInterceptor.Level.BODY
        }
    
    @Provides
    fun okHttpClientProvider(
        interceptor: Interceptor
    ) = OkHttpClient
        .Builder()
        .addInterceptor(interceptor)
        .build()
    
    @Provides
    fun moshiProvider(): Moshi =
        Moshi
            .Builder()
            .add(KotlinJsonAdapterFactory())
            .build()
    
    @Provides
    fun retrofitProvider(
        okHttpClient: OkHttpClient
    ): Retrofit =
        Retrofit
            .Builder()
            .client(okHttpClient)
            .baseUrl(BASE_URL)
            .addConverterFactory(MoshiConverterFactory.create())
            .build()
    
    @Provides
    fun provideArticleWs(retrofit: Retrofit): ArticleWs = 
        retrofit.create(ArticleWs::class.java)
    
    @Provides
    fun articleDataSourceProvider(
        articleWs: ArticleWs
    ) = ArticleRemoteDataSourceImpl(articleWs) as ArticleRemoteDataSource
}
Hilt automatically resolves the dependency chain:
  1. HttpLoggingInterceptor provided by interceptorProvider()
  2. OkHttpClient created with interceptor
  3. Moshi JSON converter created
  4. Retrofit created with client and base URL
  5. ArticleWs Retrofit service created
  6. ArticleRemoteDataSourceImpl created with service
All dependencies in SingletonComponent are created once and reused:
  • Single Retrofit instance
  • Single OkHttpClient instance
  • Single data source instances
  • Single repository instances

Data layer: LocalModule

Provides Room database and local data sources:
package es.mobiledev.data.local.di

import android.app.Application
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import es.mobiledev.data.local.AppRoomDatabase
import es.mobiledev.data.local.article.datasource.ArticleLocalDataSourceImpl
import es.mobiledev.data.source.article.ArticleLocalDataSource
import javax.inject.Singleton

@Module
@InstallIn(SingletonComponent::class)
object LocalModule {
    @Provides
    @Singleton
    fun appRoomDatabaseProvider(
        context: Application
    ) = AppRoomDatabase.buildDatabase(context)
    
    @Provides
    @Singleton
    fun articleLocalDataSourceProvider(
        roomDatabase: AppRoomDatabase
    ) = ArticleLocalDataSourceImpl(roomDatabase) as ArticleLocalDataSource
}
Always annotate database providers with @Singleton to prevent multiple database instances, which would cause crashes.

Data layer: SessionModule

Provides SharedPreferences data source:
package es.mobiledev.data.session.di

import android.app.Application
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import es.mobiledev.data.session.PreferencesDataSourceImpl
import es.mobiledev.data.source.preferences.PreferencesDataSource
import javax.inject.Singleton

@Module
@InstallIn(SingletonComponent::class)
object SessionModule {
    @Provides
    @Singleton
    fun preferencesDataSourceProvider(
        context: Application
    ) = PreferencesDataSourceImpl(context) as PreferencesDataSource
}

ViewModel injection

Hilt automatically injects dependencies into ViewModels annotated with @HiltViewModel.

ViewModel with dependencies

package es.mobiledev.feature.home.viewmodel

import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
import es.mobiledev.commonandroid.ui.base.BaseViewModel
import es.mobiledev.commonandroid.ui.base.UiState
import es.mobiledev.domain.model.article.ArticleBo
import es.mobiledev.domain.usecase.article.GetArticlesUseCase
import es.mobiledev.domain.usecase.article.GetFavoriteArticlesUseCase
import es.mobiledev.domain.usecase.article.SaveOrRemoveFavoriteArticleUseCase
import es.mobiledev.domain.usecase.preferences.GetLastOpenTimeUseCase
import es.mobiledev.domain.usecase.preferences.SaveLastOpenTimeUseCase
import es.mobiledev.feature.home.state.HomeUiState
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
import javax.inject.Inject

@HiltViewModel
class HomeViewModel @Inject constructor(
    private val getArticlesUseCase: GetArticlesUseCase,
    private val getFavoriteArticlesUseCase: GetFavoriteArticlesUseCase,
    private val saveOrRemoveFavoriteArticleUseCase: SaveOrRemoveFavoriteArticleUseCase,
    private val saveLastOpenTimeUseCase: SaveLastOpenTimeUseCase,
    private val getLastOpenTimeUseCase: GetLastOpenTimeUseCase,
) : BaseViewModel<HomeUiState>() {
    override val uiState: MutableStateFlow<UiState<HomeUiState>> = 
        MutableStateFlow(value = UiState(data = HomeUiState()))
    
    init {
        viewModelScope.launch(Dispatchers.IO) {
            fetchData()
        }
    }
    
    private suspend fun getArticles() =
        getArticlesUseCase(limit = 5L, offset = 0L).collectLatest { response ->
            uiState.successState { currentUiState ->
                currentUiState.copy(articles = response.results)
            }
        }
    
    fun onFavoriteClick(article: ArticleBo, isFavorite: Boolean) {
        viewModelScope.launch(Dispatchers.IO) {
            try {
                saveOrRemoveFavoriteArticleUseCase(article, isFavorite)
                getFavoriteArticles()
            } catch (e: Exception) {
                // Error handling
            }
        }
    }
}
1

Annotate ViewModel

Add @HiltViewModel to the ViewModel class
2

Constructor injection

Add @Inject constructor() with dependencies as parameters
3

Hilt resolves dependencies

Hilt automatically provides use cases through the dependency graph
4

Obtain in Composable

Use hiltViewModel() in Composables to get the injected ViewModel

Using ViewModels in Composables

import androidx.hilt.navigation.compose.hiltViewModel

@Composable
fun HomeScreen(
    viewModel: HomeViewModel = hiltViewModel()
) {
    val uiState by viewModel.getUiState().collectAsStateWithLifecycle()
    
    // Use uiState in your UI
}
The hiltViewModel() function retrieves the ViewModel from Hilt’s ViewModelComponent, automatically handling scoping.

Dependency graph visualization

Here’s how Hilt wires dependencies together:

Testing with Hilt

Hilt provides testing utilities to replace dependencies with mocks.

Test setup

import dagger.hilt.android.testing.HiltAndroidRule
import dagger.hilt.android.testing.HiltAndroidTest
import org.junit.Rule
import org.junit.Test

@HiltAndroidTest
class HomeViewModelTest {
    @get:Rule
    var hiltRule = HiltAndroidRule(this)
    
    @Test
    fun testArticleFetch() {
        // Hilt automatically provides dependencies
    }
}

Replacing dependencies

import dagger.hilt.android.testing.HiltAndroidTest
import dagger.hilt.android.testing.UninstallModules
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent

@UninstallModules(RepositoryModule::class)
@HiltAndroidTest
class HomeViewModelTest {
    @Module
    @InstallIn(SingletonComponent::class)
    object FakeRepositoryModule {
        @Provides
        fun provideFakeArticleGateway(): ArticleGateway = FakeArticleGateway()
    }
    
    // Tests use FakeArticleGateway instead of real implementation
}
Use @UninstallModules to remove production modules and @TestInstallIn to provide test doubles.

Best practices

Always use constructor injection (@Inject constructor) instead of field injection. Constructor injection is:
  • Easier to test (can create objects without Hilt)
  • More explicit about dependencies
  • Compile-time safe
Provider functions should return interface types, not implementations:
@Provides
fun articleRepositoryProvider(
    remote: ArticleRemoteDataSource,
    local: ArticleLocalDataSource
) = ArticleRepository(remote, local) as ArticleGateway  // ✅ Returns interface
  • @Singleton / SingletonComponent: For app-wide singletons (databases, repositories)
  • ViewModelComponent: For ViewModel-scoped dependencies (automatic)
  • ActivityRetainedComponent: For dependencies that survive configuration changes
  • No scope: New instance every time
Each Hilt module should provide related dependencies:
  • RemoteModule: Networking dependencies
  • LocalModule: Database dependencies
  • RepositoryModule: Repository implementations
  • UseCaseModule: Use case implementations
When providing multiple instances of the same type, use qualifiers:
@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class IoDispatcher

@Provides
@IoDispatcher
fun provideIoDispatcher(): CoroutineDispatcher = Dispatchers.IO

Common patterns

Providing context

Hilt automatically provides Application context:
@Provides
fun provideSomeService(
    context: Application  // Automatically injected
): SomeService = SomeServiceImpl(context)

Binding interfaces to implementations

Use @Binds for simple interface-to-implementation bindings (more efficient than @Provides):
@Module
@InstallIn(SingletonComponent::class)
abstract class DataSourceModule {
    @Binds
    abstract fun bindArticleRemoteDataSource(
        impl: ArticleRemoteDataSourceImpl
    ): ArticleRemoteDataSource
}
@Binds is more efficient than @Provides because it doesn’t require an implementation body.

Providing coroutine dispatchers

@Module
@InstallIn(SingletonComponent::class)
object DispatcherModule {
    @Provides
    @IoDispatcher
    fun provideIoDispatcher(): CoroutineDispatcher = Dispatchers.IO
    
    @Provides
    @MainDispatcher
    fun provideMainDispatcher(): CoroutineDispatcher = Dispatchers.Main
}

Troubleshooting

Missing binding error: Ensure all modules are included in the app module’s dependencies and that @InstallIn is present.
Duplicate binding error: Check for multiple @Provides functions returning the same type without qualifiers.
Build fails after adding Hilt: Make sure you’ve applied the Hilt plugin and added the KSP dependency.

Next steps

Architecture overview

Review overall architecture

Clean architecture

Understand clean architecture principles

Modules

Explore module organization

Build docs developers (and LLMs) love