Integrating Hilt Dependency Injection in Android Compose Applications

Angel SaikiaAngel Saikia
3 min read

Introduction

Hilt is the recommended dependency injection library for Android, built on top of Dagger to simplify DI implementation. This guide will walk you through integrating Hilt in your Android Compose application, using practical examples from a fitness tracking app.

Setup

1. Add Dependencies

First, add the required dependencies in your project's build.gradle.kts file:

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 Application Class

Create a custom Application class and annotate it with @HiltAndroidApp:

@HiltAndroidApp
class MyApplication : Application()

Update your AndroidManifest.xml to use the custom Application class:

<application
    android:name=".MyApplication"
    ... >

Dependency Injection Implementation

1. Creating Modules

Modules are classes that tell Hilt how to provide instances of different types. Here's an example of a network module:

@Module
@InstallIn(SingletonComponent::class)
object NetworkModule {
    @Provides
    @Singleton
    fun provideApiService(): ApiService {
        return Retrofit.Builder()
            .baseUrl("https://api.example.com/")
            .addConverterFactory(GsonConverterFactory.create())
            .build()
            .create(ApiService::class.java)
    }

    @Provides
    @Singleton
    fun provideRepository(apiService: ApiService, app: Application): MyRepository {
        return MyRepositoryImpl(apiService, app)
    }
}

2. Implementing ViewModels

To use Hilt in ViewModels, annotate them with @HiltViewModel and inject dependencies:

@HiltViewModel
class HomeViewModel @Inject constructor(
    private val repository: MyRepository
) : ViewModel() {
    private val _uiState = MutableStateFlow(HomeUiState())
    val uiState: StateFlow<HomeUiState> = _uiState.asStateFlow()

    init {
        loadData()
    }

    private fun loadData() {
        viewModelScope.launch {
            try {
                val data = repository.getData()
                _uiState.value = _uiState.value.copy(
                    data = data,
                    isLoading = false
                )
            } catch (e: Exception) {
                _uiState.value = _uiState.value.copy(
                    error = e.message,
                    isLoading = false
                )
            }
        }
    }
}

3. Using in Composables

In your Composable functions, use hiltViewModel() to get an instance of your ViewModel:

@Composable
fun HomeScreen(
    viewModel: HomeViewModel = hiltViewModel()
) {
    val uiState by viewModel.uiState.collectAsState()

    when {
        uiState.isLoading -> LoadingIndicator()
        uiState.error != null -> ErrorMessage(uiState.error)
        else -> Content(uiState.data)
    }
}

Best Practices

  1. Scope Management: Use appropriate scope annotations:

    • @Singleton for application-wide instances

    • @ActivityScoped for activity-level instances

    • @ViewModelScoped for ViewModel-level instances

  2. Testing: Hilt makes testing easier by allowing you to swap implementations:

@Module
@TestInstallIn(
    components = [SingletonComponent::class],
    replaces = [NetworkModule::class]
)
object TestNetworkModule {
    @Provides
    @Singleton
    fun provideTestApiService(): ApiService {
        return FakeApiService()
    }
}
  1. Interface Abstraction: Always depend on interfaces rather than concrete implementations:
interface MyRepository {
    suspend fun getData(): List<Data>
}

class MyRepositoryImpl @Inject constructor(
    private val apiService: ApiService
) : MyRepository {
    override suspend fun getData(): List<Data> {
        return apiService.getData()
    }
}

Common Pitfalls

  1. Circular Dependencies: Avoid circular dependencies between classes. Use @Lazy if necessary.

  2. Scope Mismatch: Ensure that dependencies have compatible scopes. A @Singleton component cannot depend on a @ActivityScoped component.

  3. Missing Bindings: Always provide all necessary dependencies in your modules.

Conclusion

Hilt simplifies dependency injection in Android applications by reducing the boilerplate code and providing a standard way to implement DI. When used with Jetpack Compose, it creates a clean and maintainable architecture that's easy to test and scale.

Remember to:

  • Keep modules focused and organized

  • Use appropriate scopes

  • Follow interface-based design

  • Write tests using Hilt's testing utilities

By following these guidelines, you'll have a robust dependency injection setup in your Android Compose application.

0
Subscribe to my newsletter

Read articles from Angel Saikia directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Angel Saikia
Angel Saikia