Files
vnidrop/.codex/skills/compose-skill/references/hilt.md
Hammed Abass fd4fff2c86 docs: add compose development guidelines and skills
Added a comprehensive collection of Compose development guidelines, best practices, and skill definitions in the .codex directory.

Co-authored-by: Junie <junie@jetbrains.com>
2026-07-07 16:55:43 +02:00

8.8 KiB

Dependency Injection with Hilt (Android-only)

Compile-time DI for Android-only Compose projects with ViewModel and lifecycle integration.

For Hilt vs Koin decision guidance and shared DI concepts, see dependency-injection.md. For Koin (multiplatform), see koin.md.

References:

Setup

Gradle configuration

// project-level build.gradle.kts
plugins {
    alias(libs.plugins.hilt) apply false
}

// app-level build.gradle.kts
plugins {
    alias(libs.plugins.android.application)
    alias(libs.plugins.kotlin.android)
    alias(libs.plugins.hilt)
    alias(libs.plugins.ksp)
}

dependencies {
    implementation(libs.hilt.android)
    ksp(libs.hilt.compiler)
    
    // Compose integration
    implementation(libs.hilt.navigation.compose)
}

Application Class

@HiltAndroidApp
class MyApplication : Application()

Every Hilt app requires an @HiltAndroidApp-annotated Application class.

Modules

@Provides — when you need to construct the instance yourself

Use for third-party classes, builder patterns, or anything where you control creation logic:

@Module
@InstallIn(SingletonComponent::class)
object AppModule {
    @Provides
    @Singleton
    fun provideApiClient(): ApiClient = ApiClient()
    
    @Provides
    @Singleton
    fun provideDatabase(@ApplicationContext context: Context): AppDatabase =
        Room.databaseBuilder(context, AppDatabase::class.java, "app.db").build()
}

@Binds — when mapping an interface to its implementation

Use for interface-to-implementation bindings. More efficient than @Provides (no method body needed, generates less code):

@Module
@InstallIn(SingletonComponent::class)
abstract class RepositoryModule {
    @Binds
    @Singleton
    abstract fun bindUserRepository(impl: UserRepositoryImpl): UserRepository
    
    @Binds
    @Singleton
    abstract fun bindProductRepository(impl: ProductRepositoryImpl): ProductRepository
}

Feature-scoped modules — @InstallIn(ViewModelComponent)

Use ViewModelComponent when dependencies are only needed within a ViewModel and should be cleaned up when the ViewModel is cleared. Use SingletonComponent for app-wide shared instances (API clients, databases).

@Module
@InstallIn(ViewModelComponent::class)
object ProductModule {
    @Provides
    @ViewModelScoped
    fun provideProductCalculator(): ProductCalculator = ProductCalculator()
    
    @Provides
    @ViewModelScoped
    fun provideProductValidator(): ProductValidator = ProductValidator()
}

ViewModel Injection

Basic ViewModel

@HiltViewModel
class ProductViewModel @Inject constructor(
    private val calculator: ProductCalculator,
    private val repository: ProductRepository,
) : ViewModel() {
    // StateFlow<State>, Channel<Effect>, onEvent() — see architecture.md
}

ViewModel with SavedStateHandle — when params come from navigation routes

Hilt auto-injects SavedStateHandle populated with navigation arguments. Use when the ViewModel receives serializable route params:

@HiltViewModel
class DetailViewModel @Inject constructor(
    private val repository: ItemRepository,
    savedStateHandle: SavedStateHandle,
) : ViewModel() {
    private val itemId: String = checkNotNull(savedStateHandle["itemId"])
    
    init {
        loadItem(itemId)
    }
}

ViewModel with @AssistedInject — when params come from the caller, not navigation

Use when the ViewModel needs values that aren't in navigation arguments (e.g., a complex object, a callback, or a value computed in the composable):

@HiltViewModel(assistedFactory = DetailViewModel.Factory::class)
class DetailViewModel @AssistedInject constructor(
    private val repository: ItemRepository,
    @Assisted private val itemId: String,
) : ViewModel() {
    
    @AssistedFactory
    interface Factory {
        fun create(itemId: String): DetailViewModel
    }
}

// Caller passes the value explicitly
@Composable
fun DetailRoute(itemId: String) {
    val viewModel = hiltViewModel<DetailViewModel, DetailViewModel.Factory> { factory ->
        factory.create(itemId)
    }
}

Prefer SavedStateHandle for navigation arguments (simpler, survives process death). Use @AssistedInject only when SavedStateHandle can't carry the data.

Compose Integration

@AndroidEntryPoint
class MainActivity : ComponentActivity() { /* setContent { ... } */ }

@Composable
fun ProductRoute(viewModel: ProductViewModel = hiltViewModel()) {
    val state by viewModel.state.collectAsStateWithLifecycle()
    ProductScreen(state = state, onEvent = viewModel::onEvent)
}

Every Activity hosting Hilt-injected composables requires @AndroidEntryPoint. Use the standard MVI Route/Screen pattern: collect state via collectAsStateWithLifecycle(), collect effects via CollectEffect, pass onEvent to Screen.

Navigation Integration

For Nav 3 + Hilt patterns (entry-scoped ViewModels, multibinding entry providers), see navigation-3-di.md — that is the preferred approach for new projects. For Nav 2 + Hilt patterns (graph-scoped VMs, @AssistedInject), see navigation-2-di.md.

The patterns below apply to Navigation Compose (Nav 2) projects that use Hilt. They remain valid for existing codebases but should not be the starting point for new work.

Nav 2: hiltViewModel() in composable destinations

Use hiltViewModel() as a default parameter in any composable() destination — each destination gets its own ViewModel instance scoped to the NavBackStackEntry.

Nav 2: Navigation-scoped ViewModel — when multiple destinations share state

Use when destinations within the same Nav 2 navigation graph need a shared ViewModel (e.g., a multi-step checkout flow where Cart, Shipping, and Payment screens share CheckoutViewModel):

val parentEntry = remember(navController) {
    navController.getBackStackEntry("checkout_graph")
}
val sharedViewModel: CheckoutViewModel = hiltViewModel(parentEntry)

Scopes

Scope Lifecycle Use case
@Singleton Application API clients, databases, shared preferences
@ActivityRetainedScoped Activity (survives config change) User session, auth state
@ViewModelScoped ViewModel Feature-specific services, calculators
@ActivityScoped Activity instance Activity-bound resources
@FragmentScoped Fragment instance Fragment-bound resources (rare in Compose)

Hilt in MVI

The only Hilt-specific wiring is @HiltViewModel + @Inject constructor. The MVI pattern (Event/State/Effect, onEvent()) is framework-agnostic — DI only affects constructor injection and injection-site calls.

Testing

For ViewModel unit tests (no Hilt needed), see testing.md.

Dependencies

dependencies {
    androidTestImplementation(libs.hilt.android.testing)
    kspAndroidTest(libs.hilt.compiler)
}

Hilt instrumented testing

@HiltAndroidTest
class CreateItemScreenTest {
    @get:Rule(order = 0)
    val hiltRule = HiltAndroidRule(this)
    
    @get:Rule(order = 1)
    val composeRule = createAndroidComposeRule<MainActivity>()
    
    @Inject
    lateinit var repository: ItemRepository
    
    @Before
    fun setup() {
        hiltRule.inject()
    }
    
    @Test
    fun saveButton_enabledWhenFieldsFilled() {
        composeRule.setContent {
            CreateItemScreen(
                state = CreateItemState(title = "Test", amount = "100"),
                onEvent = {},
            )
        }
        
        composeRule.onNodeWithText("Save").assertIsEnabled()
    }
}

@Module
@InstallIn(SingletonComponent::class)
@TestInstallIn(components = [SingletonComponent::class], replaces = [RepositoryModule::class])
object FakeRepositoryModule {
    @Provides
    @Singleton
    fun provideItemRepository(): ItemRepository = FakeItemRepository()
}

Anti-Patterns

Anti-pattern Why it is harmful Better approach
Injecting Context into ViewModel Lifecycle mismatch, leaks Use @ApplicationContext or move platform code to Repository
Injecting Activity/Fragment into ViewModel Memory leaks Pass data via SavedStateHandle or route arguments
@Inject on ViewModel without @HiltViewModel ViewModel not managed by Hilt Always use @HiltViewModel with @Inject constructor
Manual ViewModel instantiation Bypasses Hilt injection Use hiltViewModel() in Compose
Installing ViewModel dependencies in SingletonComponent Unnecessary lifecycle extension Use ViewModelComponent or ViewModelScoped