Simplifying Dependency Injection in Android Compose with Hilt

Integrate Hilt into your Jetpack Compose app to simplify dependency injection with minimal boilerplate. This guide walks through setup, modules, ViewModels, and best practices using a real-world fitness app example.

Vanshika Sharma

2 months ago

simplifying-dependency-injection-in-android-compose-with-hilt

Hilt, a dependency injection (DI) library built on top of Dagger, streamlines DI setup in Android apps. This tutorial demonstrates how to integrate Hilt into a Jetpack Compose-based app, using examples from a fitness tracking project.

Initial Setup

1. Add Hilt Dependencies

To begin, include the following in your build.gradle.kts file:

kotlin

plugins {

id("com.google.dagger.hilt.android")

id("kotlin-kapt")

}

dependencies {

implementation("com.google.dagger:hilt-android:2.48")

kapt("com.google.dagger:hilt-compiler:2.48")

implementation("androidx.hilt:hilt-navigation-compose:1.1.0")

}

2. Create a Custom Application Class

Create an Application subclass and annotate it with @HiltAndroidApp:

kotlin

@HiltAndroidApp class MyApp :

Application()

Then, reference this class in your AndroidManifest.xml:

xml

<application

android:name=".MyApp"

... >

</application>

Implementing Dependency Injection

1. Define Modules

Modules specify how to construct and provide dependencies. For example, a network module might look like:

kotlin

@Module

@InstallIn(SingletonComponent::class)

object NetworkModule {

@Provides

@Singleton

fun provideApi(): ApiService {

return Retrofit.Builder()

.baseUrl("https://api.example.com/")

.addConverterFactory(GsonConverterFactory.create())

.build()

.create(ApiService::class.java)

}

@Provides

@Singleton

fun provideRepo(api: ApiService, app: Application): Repository {

return RepositoryImpl(api, app)

}

}

2. Inject Dependencies in ViewModels

To make dependencies available in your ViewModel, annotate it with @HiltViewModel and use constructor injection:

kotlin

@HiltViewModel

class DashboardViewModel @Inject constructor(

private val repo: Repository

) : ViewModel() {

private val _state = MutableStateFlow(DashboardState())

val state: StateFlow<DashboardState> = _state

init { loadDashboard() }

private fun loadDashboard() {

viewModelScope.launch {

try {

val items = repo.fetchData()

_state.value = _state.value.copy(data = items, isLoading = false)

} catch (e: Exception) {

_state.value = _state.value.copy(error = e.message, isLoading = false)

}

}

}

}

3. Access ViewModels in Composables

Use hiltViewModel() to retrieve your ViewModel in a Composable:

kotlin

@Composable

fun DashboardScreen(viewModel: DashboardViewModel = hiltViewModel()) {

val state by viewModel.state.collectAsState()

when {

state.isLoading -> LoadingUI()

state.error != null -> ErrorUI(state.error)

else -> DataUI(state.data)

}

}

Best Practices

  • Scope Wisely: Use appropriate scoping annotations like @Singleton, @ActivityScoped, or @ViewModelScoped depending on the lifecycle.

  • Test with Ease: Hilt enables swapping out production modules for test modules using @TestInstallIn. Example:

    kotlin

    @Module

    @TestInstallIn(

    components = [SingletonComponent::class],

    replaces = [NetworkModule::class]

    )

    object FakeNetworkModule {

    @Provides

    @Singleton

    fun fakeApi(): ApiService = FakeApiService()

    }

  • Favor Abstractions: Code against interfaces for better testability and flexibility:

    kotlin

    interface Repository {

    suspend fun fetchData(): List<Item>

    }

    class RepositoryImpl @Inject constructor(

    private val api: ApiService

    ) : Repository {

    override suspend fun fetchData() = api.getData()

    }

Common Issues to Avoid

  • Circular References: Watch out for circular dependencies. Use @Lazy if deferring creation helps.

  • Scope Conflicts: Make sure injected types share compatible scopes.

  • Missing Providers: Ensure every injected type has a corresponding provider in a module.

Final Thoughts

Hilt offers a clean, structured way to manage dependencies in Android apps, especially when combined with Jetpack Compose. It reduces setup complexity, encourages testability, and helps enforce good architecture patterns.

To get the most out of Hilt:

  • Modularize your DI setup.

  • Maintain clear scope boundaries.

  • Depend on abstractions.

  • Write isolated, testable code using DI features.

With Hilt, your Compose applications become easier to scale, test, and maintain.