Dependency Injection with Hilt and Compose: A Complete Guide

By Engineering Team • Published: 2025-12-14 • Updated: 2025-12-1414 min read

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.
← Back to Blog