Android Hilt

Simple DI for the rest of us.

Siamak (Ash) Ashrafi
10 min readMay 10, 2024
Building next Android App using Hilt DI while watching KPOP.

This will be updated after Google I/O 2024

Google I/O is happening next week

Streamline Dependency Injection in Compose with Hilt

Managing dependencies in Android development can be cumbersome, often leading to boilerplate code and tight coupling between components. Here’s where Hilt shines! Hilt, an extension of Dagger 2, simplifies dependency injection (DI) in Kotlin/Compose applications. It automates dependency creation and management, resulting in cleaner code and improved maintainability. This guide explores key aspects of using Hilt with Compose, including ViewModel, Navigation, Room, and testing functionalities.

Why Kotlin and Hilt?

Hilt is the Swiss Arm Knife of Kotlin/Compose Android Dev. And Kotlin/Compose is the future of Android.

  • Kotlin/Compose is the future of Android development.
  • Hilt simplifies DI compared to Dagger 2.

Dagger Hilt @ Kotlin/Compose Android

Hilt and Jetpack Libraries: A Powerful Combination

Kotlin Dagger Hilt simplifies dependency injection (DI) for Android development. It integrates seamlessly with various Jetpack libraries, offering a clean and efficient way to manage dependencies within your application. Here’s a breakdown of Hilt’s support for some key Jetpack components:

Benefits of Using Hilt with Jetpack Libraries:

  • Reduced Boilerplate: Hilt automates dependency injection tasks, reducing the amount of code you need to write.
  • Improved Testability: Hilt facilitates mocking dependencies during unit tests, isolating the components under test.
  • Cleaner Code: By managing dependencies through Hilt, your code becomes more concise and easier to understand.
  • Maintainability: Hilt promotes a centralized approach to dependency management, making it easier to maintain your application as it grows.

Let’s count the ways Hilts is the center of Android development.

Hilt Usage in Android Development

Compose ViewModel Injection

Effortless ViewModel Injection with Hilt in Compose

Hilt streamlines injecting view models into your Compose UI with the @HiltViewModel annotation. This annotation leverages Hilt's model factory behind the scenes, taking care of creating and managing the lifecycle of your view models. Say goodbye to manual provider classes or factory methods, reducing boilerplate code and keeping your composables clean.

Hilt empowers you to inject view models directly into your Compose UI using the hiltViewModel() composable function. This function simplifies accessing view models created with the @HiltViewModel annotation, eliminating the need for explicit parameter passing and promoting cleaner composables.

Add the Dependency

Include the `androidx.lifecycle:lifecycle-viewmodel-compose` dependency in your TOML file to enable Hilt's ViewModel support:

androidx-lifecycle-viewmodel-compose = { module = "androidx.lifecycle:lifecycle-viewmodel-compose", version.ref = "androidxLifecycle" }

Mark your ViewModel class with @HiltViewModel. This tells Hilt to provide the necessary dependencies through constructor injection during the view model's creation.

@HiltViewModel
class MyViewModel @Inject constructor(
private val someRepo: SomeRepository // or UseCase
) {
// ... your view model logic here
}

Retrieve the ViewModel with hiltViewModel():

Within your composable function, use hiltViewModel() to access the view model instance created by Hilt. This eliminates the need to pass the view model as a parameter.

@Composable
fun MyScreen() {
val viewModel: MyViewModel = hiltViewModel() // Get the view model instance

// Use the viewModel within your composable logic
Text(text = viewModel.someData)
}

Benefits of Using @HiltViewModel:

  • Reduced Boilerplate: Hilt automates view model creation and lifecycle management, eliminating the need for manual provider classes.
  • Improved Readability: Clean composables with explicit view model dependencies enhance code clarity.
  • Simplified Testing: Hilt facilitates mocking dependencies during unit tests, leading to more isolated and reliable tests for your view models.

By embracing @HiltViewModel, you can achieve seamless dependency injection for your view models in Compose, keeping your code concise and efficient.

Compose Navigation Injection

Leverage hiltViewModel composable function from androidx.hilt:hilt-navigation-compose to easily access view models within composables. This eliminates manual dependency passing and improves testability.
— By leveraging Kotlin Dagger Hilt with Compose navigation, you can build scalable and maintainable Compose applications with a clean separation of concerns. Hilt manages dependencies, while Compose handles the UI, resulting in a streamlined development experience. Remember to explore advanced features like scoped dependencies and qualifiers as your application grows in complexity.

Seamless ViewModel Access in Compose Navigation with Hilt

Hilt empowers you to effortlessly access view models within your Compose navigation flow using the hiltViewModel() composable function. This eliminates the need for manual dependency passing between composables, promoting cleaner code and improved testability.

Add the Dependency

Include the androidx.hilt:hilt-navigation-compose dependency in your TOML file to enable Hilt's navigation support:

# Navigation with Hilt androidx-hilt-navigation-compose = { module = "androidx.hilt:hilt-navigation-compose", version.ref = "androidxHilt" }

Leveraging hiltViewModel():

  • In the Source Composable (Passing Arguments):

Navigate to another composable while passing arguments using NavController.navigate().

val userId = 10L
val navController = rememberNavController()
navController.navigate("userDetails/$userId")
  • In the Destination Composable (Accessing Arguments and ViewModel):
  1. Retrieve arguments using currentBackStackEntry?.arguments.
  2. Extract the desired value (e.g., userId in this case).
  3. Use hiltViewModel() to access the view model associated with the current navigation graph.
val userId = remember {
currentBackStackEntry?.arguments?.getString("userId")?.toLong() ?: 0L
}
val viewModel: UserDetailsViewModel = hiltViewModel()

// Use userId and viewModel in your composable logic
Text(text = "User ID: $userId")
// Use viewModel to access and display user data

Benefits of Using hiltViewModel() in Navigation:

  • Clean Composable Code: Avoids the need to explicitly pass view models between composables, resulting in more concise and readable code.
  • Improved Testability: Hilt facilitates mocking dependencies during unit tests, leading to more isolated and reliable tests for your view models.
  • Centralized Management: Hilt manages view model creation and lifecycle within the navigation graph, simplifying your composable logic.

By embracing hiltViewModel() in Compose navigation, you can achieve a smooth and efficient flow of data and view models within your application, promoting maintainability and a streamlined development experience.

Room Database Injection

Integrate Room with Hilt to manage the lifecycle of your database and inject DAOs (Data Access Objects) into repositories or directly into view models. This simplifies data access and promotes better testability.

Room Database Injection with Hilt: Simplified Data Access

Hilt simplifies integrating Room with your Compose application for streamlined data access and improved testability. Here’s how to effectively inject DAOs (Data Access Objects) into your repositories or directly into view models:

Benefits:

  • Centralized Management: Hilt manages the Room database lifecycle, ensuring proper creation and cleanup.
  • Simplified Data Access: Inject DAOs directly into your components (repositories or view models) for convenient database interaction.
  • Enhanced Testability: Hilt facilitates mocking DAOs during unit tests, allowing you to isolate and test your data access logic effectively.

Define your database class with annotations (@Database, @Entity, etc.) to specify tables, entities, and relationships.

Provide the Database

Create a module with a @Provides method to provide an instance of the Room database:

@Module
@InstallIn(ApplicationComponent::class)
object DatabaseModule {

@Provides
@Singleton
fun provideAppDatabase(@ApplicationContext appContext: Context): PhotodoDB {
return Room.databaseBuilder(
appContext,
PhotodoDB::class.java,
PhotodoDB.DATABASE_NAME
).build()
}
}

Inject DAOs

  • In Repositories: Inject the DAO into your repository class to access database operations.
class PhotoRepository @Inject constructor(
private val photoDao: PhotodoDao
) {

suspend fun getAllPhotos(): List<Photo> = photoDao().getAllPhotos()

suspend fun insertPhoto(photo: Photo) = photoDao().insertPhoto(photo)
}
  • In View Models: You can inject the Repo/UseCase directly into your view model for simpler data access.
@HiltViewModel
class PhotoListViewModel @Inject constructor(
private val photoRepo: PhotoRepository
) {
private fun intiViewModel() {
viewModelScope.launch {
photodoRepo.allGetPhotodos()
// Update View with the latest data
// Writes to the value property of MutableStateFlow,
// adding a new element to the flow and updating all
// of its collectors
.collect { photodo ->
_uiState.value = ListPhotodoUiState.Success(photodo)
}
_uiState.value = ListPhotodoUiState.Success(emptyList())
}
}
init {
intiViewModel()
}


fun onEvent(event: ListPhotodoEvent) {
when (event) {
is ListPhotodoEvent.DeleteAllPhotodos -> {
viewModelScope.launch {
try {
Log.d("Photodo", "Del Called -- onEvent")
photodoRepo.deleteAll()
} catch (e: InvalidTodoPhotoException) {
Log.d("Photodo", "Error $e")
}
}
}

is ListPhotodoEvent.DeletePhotodos -> {
viewModelScope.launch {
try {
photodoRepo.delete(event.photodo)
} catch (e: InvalidTodoPhotoException) {
Log.d("Photodo", "Error $e")
}
}
}

ListPhotodoEvent.GetListPhotodos -> TODO()
}
}
}

By following these steps and leveraging Hilt for Room database injection, you can achieve a cleaner and more maintainable approach to data access in your Compose applications.

WorkManager Injection

WorkManager Integration with androidx.hilt:hilt-work:

The androidx.hilt:hilt-work library allows you to inject dependencies into your worker classes used with WorkManager. This enables you to leverage Hilt's dependency management for background tasks, ensuring workers have access to the necessary resources for proper execution.

WorkManager: Employ androidx.hilt:hilt-work to inject dependencies into your WorkManager worker classes. Hilt ensures workers have access to necessary resources.

Include:

# WorkManager with Hilt
work-hilt = { module = "androidx.hilt:hilt-work", version.ref = "androidxHilt" }
hilt-compiler = {module = "androidx.hilt:hilt-compiler", version.ref = "androidxHilt"}

Setup Code:

@HiltWorker
class ExampleWorker @AssistedInject constructor(
@Assisted appContext: Context,
@Assisted workerParams: WorkerParameters,
workerDependency: WorkerDependency
) : Worker(appContext, workerParams) { ... }

And use it

@HiltAndroidApp
class ExampleApplication : Application(), Configuration.Provider {

@Inject lateinit var workerFactory: HiltWorkerFactory

override fun getWorkManagerConfiguration() =
Configuration.Builder()
.setWorkerFactory(workerFactory)
.build()
}

Retrofit Injection

Interfaces are not the only case where you cannot constructor-inject a type. Constructor injection is also not possible if you don’t own the class because it comes from an external library (classes like Retrofit, OkHttpClient, or Room databases), or if instances must be created with the builder pattern.

@Module
@InstallIn(ActivityComponent::class)
object AnalyticsModule {

@Provides
fun provideAnalyticsService(
// Potential dependencies of this type
): AnalyticsService {
return Retrofit.Builder()
.baseUrl("https://example.com")
.build()
.create(AnalyticsService::class.java)
}
}

Testing With Hilt

Testing with Hilt

Hilt offers functionalities specifically designed for testing Compose applications. These allow you to mock dependencies during unit tests, isolating the components under test for more robust testing practices.

Dependency Injection Testing with Hilt in Compose

Let’s discusses how to leverage Hilt for dependency injection in your Compose application’s tests. Here’s a detailed summary with code snippets:

Key Concepts:

  • Hilt in Testing: Hilt provides a seamless way to inject dependencies during testing. It automatically generates a separate set of Dagger components for each test, ensuring isolation and preventing conflicts.
  • Faking Dependencies: During testing, you often want to mock or fake dependencies to isolate your code under test. Hilt allows you to replace actual implementations with mocks or fakes in your test modules.

Add Dependencies:

  • Include the hilt-android-testing dependency in your build.gradle file:
testImplementation "com.google.dagger:hilt-android-testing:<hilt_version>"
  • Replace <hilt_version> with the actual Hilt version you're using.

Create Test Module:

  • Create a new module named androidTest or test (depending on your project structure) within your test source set.
  • Inside this module, create classes with the @Module annotation to define how to provide mocked or fake dependencies for your tests.

Example: Faked Network Client

// FakeNetworkClient.kt (test module)

@Module
@Provides
fun provideOkHttpClient(): OkHttpClient {
// Create a fake OkHttpClient that simulates real network behavior
return FakeOkHttpClient()
}

class FakeOkHttpClient : OkHttpClient() {
// Define methods to simulate network responses and behavior for your tests
}

Replacing Bindings:

  • Hilt uses bindings to define how to provide dependencies. During testing, you can replace these bindings with your mocked or fake implementations in the test module.
  • Annotate your test class with @ hiltAndroidTest to enable Hilt testing.

Example: Replacing Network Client Binding

// MyViewModelTest.kt

@RunWith(AndroidJUnit4::class)
@HiltAndroidTest
class MyViewModelTest {

@get:Rule
val hiltRule = ActivityScenarioRule(MyActivity::class.java)

@Test
fun testMyViewModel() {
val viewModel = hiltRule.activity.viewModel

// Use your faked OkHttpClient here...
}

@Module
@Provides(overrides = true)
fun provideOkHttpClient(): OkHttpClient {
// Provide your mock OkHttpClient implementation here
}
}

Hilt for Robust Testing in Kotlin/Compose Android Applications (Fakes over Mocks)

Hilt, beyond simplifying dependency injection (DI) in your Compose application, empowers you with robust testing practices. Here’s how Hilt streamlines unit testing in your project, focusing on Fakes as a preferred alternative to Mocks.

1. Isolate Components with Fakes:

  • Hilt allows you to inject fake dependencies during unit tests. Unlike mocks, fakes are lightweight implementations of your dependencies that provide controlled behavior for testing purposes.
  • Fakes mimic the real dependencies’ interfaces but have limited functionality specific to your testing needs. This isolates the component under test from external factors, leading to more reliable and focused tests.

Benefits of Using Fakes Over Mocks:

  • Simpler and Easier to Maintain: Fakes are typically simpler to create and maintain compared to mocks, which often require extensive configuration.
  • Clearer Test Intent: Fakes explicitly define the expected behavior during testing, enhancing test readability.
  • Reduced Coupling: Fakes often have less coupling to the actual implementation details compared to mocks, improving test isolation.

By leveraging Hilt for testing with Fakes, you can write more concise, maintainable, and focused unit tests for your Compose applications, ensuring their functionality and reliability.

Summary of Hilt Usage in Android Development

Hilt simplifies dependency injection (DI) in Android applications, especially for Kotlin and Compose development. Here’s a breakdown of its key functionalities:

Core Functionality

  • Reduces boilerplate code for dependency injection
  • Automatically creates and manages dependencies during runtime.

Integration with Jetpack Libraries

ViewModel

  • Use @HiltViewModel annotation to inject dependencies directly into view models.
  • Access view models in composables with hiltViewModel() function.

Navigation

  • Leverage hiltViewModel() in navigation composables to access view models from different parts of the navigation flow.

Room

  • Inject DAOs (Data Access Objects) into repositories then ViewModel for simplified data access.

WorkManager

  • Inject dependencies into worker classes for background tasks.

Retrofit:

  • Use modules to provide instances of classes like Retrofit or OkHttpClient with custom configurations.

Testing

  • hilt-android-testing dependency: Enables Hilt testing functionalities.

Fakes vs Mocks

  • Hilt allows replacing real dependencies with Fakes (lightweight implementations) during tests for better isolation.
  • Fakes are generally simpler to create and maintain compared to Mocks, leading to more focused and readable unit tests.

Benefits:

  • Reduced Boilerplate: Hilt automates dependency management, reducing code complexity.
  • Improved Testability: Fakes facilitate isolated testing of components.
  • Cleaner Code: Dependency management is centralized, keeping code concise.
  • Maintainability: Promotes a centralized approach for easier maintenance as the app grows.

--

--