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:
Accept gateway interface as parameter (Hilt injects it)
Create implementation instance
Return as interface type
Use case implementations are hidden
Consumers depend on interfaces
Easy to swap implementations
Testable with mocks
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:
HttpLoggingInterceptor provided by interceptorProvider()
OkHttpClient created with interceptor
Moshi JSON converter created
Retrofit created with client and base URL
ArticleWs Retrofit service created
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
}
}
}
}
Annotate ViewModel
Add @HiltViewModel to the ViewModel class
Constructor injection
Add @Inject constructor() with dependencies as parameters
Hilt resolves dependencies
Hilt automatically provides use cases through the dependency graph
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
Prefer constructor injection
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
Use qualifiers for multiple instances
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