---
name: android-hilt-di
description: "Wire Hilt dependency injection the right way — modules, scopes, ViewModels, WorkManager, testing. A senior Android engineer for Hilt. Sets up @Module + @Provides, ViewModel injection, assisted injection, @Singleton vs @ViewModelScoped, custom scopes, and the testing patterns (replaceProductionBindings, @UninstallModules) most devs get wrong."
license: MIT
---

# Android Hilt DI

A senior Android engineer that wires up Hilt dependency injection correctly the first time. It handles the parts that bite people in code review: scope mismatches, missing qualifiers, ViewModel injection that doesn't compile, and test setups that silently use production bindings. For Android developers using Hilt (Dagger) in Kotlin who want production-grade DI without spelunking through Dagger error messages.

## When to use it

- Setting up Hilt in a new module or migrating from manual Dagger.
- A `@Provides` method returns the wrong scope and you're getting duplicate instances of a `@Singleton`.
- You need to inject a ViewModel that takes runtime arguments (e.g. an `itemId` from navigation).
- Injecting into a `Worker`, a `BroadcastReceiver`, or a non-Hilt-entry-point class.
- Your instrumented tests pass with the real network layer because the fake binding never replaced it.

## How it works

The skill scaffolds and reviews the real Hilt surface area:

- `@HiltAndroidApp` on the Application, `@AndroidEntryPoint` on Activities/Fragments.
- `@Module` + `@InstallIn(SingletonComponent::class)` with `@Provides`, and `@Binds` for interface-to-impl mappings (placed in an `abstract class`).
- ViewModel injection via `@HiltViewModel` + `@Inject constructor`, retrieved with `hiltViewModel()` (Compose) or `by viewModels()`.
- Assisted injection with `@AssistedInject`, `@Assisted`, and an `@AssistedFactory` for runtime params.
- Scoping: `@Singleton`, `@ActivityRetainedScoped`, `@ViewModelScoped`, `@ActivityScoped`, plus `@Qualifier` annotations to disambiguate same-type bindings.
- WorkManager via `@HiltWorker` + `@AssistedInject` and the `HiltWorkerFactory` wired through `Configuration.Provider`.
- Testing with `@HiltAndroidTest`, `HiltAndroidRule`, `@UninstallModules`, `@TestInstallIn`, and `@BindValue`.

## Example

```kotlin
@HiltViewModel
class DetailViewModel @AssistedInject constructor(
  private val repo: ItemRepository,
  @Assisted val itemId: String,
) : ViewModel() {
  @AssistedFactory interface Factory { fun create(itemId: String): DetailViewModel }
}

// In a test module:
@TestInstallIn(components = [SingletonComponent::class], replaces = [NetworkModule::class])
@Module object FakeNetworkModule {
  @Provides fun api(): Api = FakeApi()
}
```

## Tips & gotchas

- Use `@Binds` over `@Provides` for interface bindings — it generates less code and can't accidentally call a constructor with the wrong scope.
- `@TestInstallIn` (not just `@UninstallModules`) is the durable way to swap bindings across a whole test source set; `@UninstallModules` is per-class.
- A `@ViewModelScoped` dependency lives as long as the ViewModel, not the screen — don't reach for `@ActivityScoped` to share state across config changes.
- Call `hiltRule.inject()` in `@Before` or your `@Inject` test fields stay null.

## FAQ

**`@Singleton` vs `@ViewModelScoped`?** `@Singleton` lives for the app process; `@ViewModelScoped` is recreated per ViewModel. Use the latter for per-screen caches you want garbage-collected when the user leaves.

**Why "cannot be provided without an @Inject constructor"?** The type has no binding in the installed component — add `@Provides`/`@Binds`, or check you used the right `@InstallIn` component.

**Does Hilt replace constructor injection?** No — it builds the graph around plain `@Inject constructor`. You only write modules for types you don't own or interfaces.
