Dependency Injection with Hilt and Compose: A Complete Guide
HiltDependency InjectionArchitectureTesting
Why Hilt for Compose?
Dependency Injection (DI) is essential for maintainable, testable code. Hilt is Google's recommended DI framework for Android, and it integrates seamlessly with Compose.
With Hilt, you can inject ViewModels, repositories, and other dependencies without manual factory creation. This reduces boilerplate and makes testing trivial.
Basic Setup
First, add Hilt dependencies and set up your Application class:
// build.gradle.kts
plugins {
id("com.google.dagger.hilt.android")
id("com.google.devtools.ksp")
}
dependencies {
implementation("com.google.dagger:hilt-android:2.50")
ksp("com.google.dagger:hilt-compiler:2.50")
implementation("androidx.hilt:hilt-navigation-compose:1.2.0")
}
// Application class
@HiltAndroidApp
class MyApplication : Application()
// MainActivity
@AndroidEntryPoint
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
MyAppTheme {
AppNavigation()
}
}
}
}Injecting ViewModels
Use @HiltViewModel and hiltViewModel() for clean ViewModel injection:
@HiltViewModel
class ProductListViewModel @Inject constructor(
private val productRepository: ProductRepository,
private val analyticsTracker: AnalyticsTracker
) : ViewModel() {
private val _products = MutableStateFlow<List<Product>>(emptyList())
val products: StateFlow<List<Product>> = _products.asStateFlow()
init {
loadProducts()
}
private fun loadProducts() {
viewModelScope.launch {
_products.value = productRepository.getProducts()
analyticsTracker.trackScreenView("product_list")
}
}
}
// In Compose - hiltViewModel() handles creation and scoping
@Composable
fun ProductListScreen(
viewModel: ProductListViewModel = hiltViewModel(),
onProductClick: (String) -> Unit
) {
val products by viewModel.products.collectAsStateWithLifecycle()
ProductList(products = products, onProductClick = onProductClick)
}Providing Dependencies with Modules
Define Hilt modules to provide dependencies that cannot be constructor-injected:
@Module
@InstallIn(SingletonComponent::class)
object NetworkModule {
@Provides
@Singleton
fun provideOkHttpClient(): OkHttpClient {
return OkHttpClient.Builder()
.addInterceptor(HttpLoggingInterceptor())
.connectTimeout(30, TimeUnit.SECONDS)
.build()
}
@Provides
@Singleton
fun provideRetrofit(okHttpClient: OkHttpClient): Retrofit {
return Retrofit.Builder()
.baseUrl("https://api.example.com/")
.client(okHttpClient)
.addConverterFactory(GsonConverterFactory.create())
.build()
}
@Provides
@Singleton
fun provideProductApi(retrofit: Retrofit): ProductApi {
return retrofit.create(ProductApi::class.java)
}
}
@Module
@InstallIn(SingletonComponent::class)
abstract class RepositoryModule {
@Binds
@Singleton
abstract fun bindProductRepository(
impl: ProductRepositoryImpl
): ProductRepository
}Testing with Hilt
Hilt makes testing easy by allowing you to replace dependencies:
@HiltAndroidTest
class ProductListScreenTest {
@get:Rule(order = 0)
val hiltRule = HiltAndroidRule(this)
@get:Rule(order = 1)
val composeRule = createAndroidComposeRule<MainActivity>()
@BindValue
val fakeRepository: ProductRepository = FakeProductRepository()
@Before
fun setup() {
hiltRule.inject()
(fakeRepository as FakeProductRepository).setProducts(
listOf(Product("1", "Test Product", 9.99))
)
}
@Test
fun productList_displaysProducts() {
composeRule.onNodeWithText("Test Product").assertIsDisplayed()
composeRule.onNodeWithText("$9.99").assertIsDisplayed()
}
}Note: Use @BindValue to replace real dependencies with fakes in tests. This is cleaner than creating separate test modules.