
The problem
Before we can truly dive into Dagger & Hilt, we need to understand the underlying problem it solves.
Throughout the development cycle, as you are introducing new features to your application you reuse the same classes, whether they are created by you or part of 3rd party libraries. Instead of building lengthy dependency chains A -> B, B -> C you can remove concern from the intermediary components. Effectively without DI (dependency injection) you will fall victim to tight coupling preventing simple reuse of components, and significant overhead in form of boilerplate code. Think about it as building a Car:
- if
Carneeds anEngineand EngineneedsPistons- to construct a
CartheCarneeds to know aboutPistons.
Dagger & Hilt allows you to reverse this process (inversion of control). The Engine knows about Pistons and how to construct it, and Car just needs to simply request an Engine.
Table of Contents
Understanding Dagger
You might often hear Hilt used with Dagger, or specifically called “Dagger Hilt” – this is simply because Hilt is abstraction layer on top of Dagger. Dagger provides the raw functionality, but it also comes with its own verbosity and complexity, and Hilt is wrapper built on top of Dagger to abstract the complexity in a way that unless for very specific cases you will not need to use Dagger by itself.
Dagger introduces several key annotations that we will use as we implement Hilt into our android application:
@Inject
Can be used to annotate either field, to say “I need this dependency injected” or a constructor to tell Dagger how to create an instance of the given class.
class Car @Inject constructor(
private val engine: Engine
) {
// Car now knows how to construct itself using Engine
}@Module
A module is used to tell Dagger that a specific class contains instructions on how to instantiate 3rd party dependencies
@Provides
Annotates a function inside of a class (Module) that returns an instance of the object to be used as dependency.
@Binds
When an interface is requested, it tells Dagger which implementation to choose.
Understanding Hilt
We already sort of established that Hilt is wrapper on top of Dagger. With that said it also introduces several annotations that you won’t see directly in Dagger.
@HiltAndroidApp
This is required annotation on top of the default Application class. It tells Hilt where to start building the dependency graph – call it a root node.
@HiltAndroidApp
class YourApp: Application()@AndroidEntryPoint
Is annotation used with views, activities, fragments, services and broadcast receivers. It tells Hilt “I’m going to use @Inject with this class”
@HiltViewModel
Special annotation that allows view models to be constructed by Hilt.
Scopes
Help us control lifecycle of dependency:
- @Singleton – single instance per application for its lifecycle
- @ActivityScoped – single instance per activity
- @FragmentScoped – single instance per fragment
@InstallIn
Is required to be used in conjunction with @Module from Dagger and defines the lifecycle of the component.
Boilerplate
1. Create new Android project with Compose Activity
2. Modify your build.gradle (Project)
plugins {
alias(libs.plugins.android.application) apply false
alias(libs.plugins.kotlin.android) apply false
alias(libs.plugins.kotlin.compose) apply false
// Hilt
id("com.google.dagger.hilt.android") version "2.57.2" apply false
}3. Add dependencies & plugins to build.gradle (App)
plugins {
// ... rest of your plugins
id("kotlin-kapt")
id("com.google.dagger.hilt.android")
}
// ... remainder of build.gradle (App) contents
dependencies {
// ... your other dependencies
implementation(libs.hilt.android)
kapt(libs.hilt.android.compiler)
// Use if you are using Jetpack Compose
implementation(libs.androidx.hilt.navigation.compose)
// Lifecycle util for Compose
implementation(libs.androidx.lifecycle.runtime.compose)
}4. Sync dependencies (Gradle)
5. Create application class in the root of your package MyApp.kt (choose your own name) that extends Application class, annotate it with @HiltAndroidApp
package dev.behindthe.dagger_hilt
import android.app.Application
import dagger.hilt.android.HiltAndroidApp
@HiltAndroidApp
class MyApp : Application()6. Open your AndroidManifest.xml and add under application property “name” referencing your newly created class
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<application
android:name=".MyApp"
//... rest of application config
<activity
// ... existing activity config
</activity>
</application>
</manifest>7. Annotate your MainActivity with @AndroidEntryPoint
@AndroidEntryPoint
class MainActivity : ComponentActivity() {
// Hilt can now inject dependencies here!
override fun onCreate(savedInstanceState: Bundle?) {
}
}Dagger & Hilt in Action
1. Create MainUiState.kt inside of the root package and respective data class.
package dev.behindthe.dagger_hilt
data class MainUiState(
val isLoading: Boolean = false,
val userId: String?
)2. Create MainViewModel.kt inside of the root of your package.
package dev.behindthe.dagger_hilt
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
import jakarta.inject.Inject
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
@HiltViewModel
class MainViewModel @Inject constructor(
// define list of Hilt supported dependencies
// private val authRepository: AuthRepository
// Hilt will handle the injection for you
) : ViewModel() {
private val _uiState = MutableStateFlow(MainUiState(
isLoading = true,
userId = null
))
val uiState: StateFlow<MainUiState> = _uiState.asStateFlow()
// Simulate
fun signIn() {
_uiState.value = _uiState.value.copy(isLoading = true)
viewModelScope.launch {
delay(2000)
_uiState.value = MainUiState(isLoading = false, userId = "123")
}
}
}3. Wire it up in your MainActivity.kt and add display elements.
package dev.behindthe.dagger_hilt
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height
import androidx.compose.material3.Button
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import dagger.hilt.android.AndroidEntryPoint
import dev.behindthe.dagger_hilt.ui.theme.DaggerhiltTheme
@AndroidEntryPoint
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
DaggerhiltTheme() {
MainScreen()
}
}
}
}
@Composable
fun MainScreen(
// Hilt knows which instance to inject here
viewModel: MainViewModel = hiltViewModel()
) {
val state by viewModel.uiState.collectAsState()
Column(
modifier = Modifier.fillMaxSize(),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
if (state.userId != null) {
Text(text = "Hello User: ${state.userId}", style = MaterialTheme.typography.headlineMedium)
} else {
Text(text = "Status: Guest", style = MaterialTheme.typography.headlineMedium)
}
Spacer(modifier = Modifier.height(16.dp))
if (state.isLoading) {
CircularProgressIndicator()
} else {
Button(
onClick = { viewModel.signIn() },
enabled = !state.isLoading
) {
Text(text = if (state.userId != null) "Signed In" else "Sign In")
}
}
}
}For the sake of simplicity, we are not following best practices for structuring projects. For best practices refer to Android Architecture.
You should now be good to get started exploring Dagger & Hilt on your own! That being said there is still a lot more to learn, I recommend reading the official docs for Dagger at least once. Followed up by the Hilt docs – the expectation shouldn’t be that you remember exactly what’s in there, but to familiarize yourself with the concepts. In the future if you come across an issue or deal with a problem you haven’t seen before, you might have memory reference to go back to the docs, just remembering it was mentioned.
Finally, if you are just starting with software development stay tuned to my blog behindthe.dev and check out my DSA (data structures & algorithms series) for more educational content!