Skip to content
intermediate Phase 6 · Architecture Patterns

Hilt Dependency Injection

Inject dependencies with Hilt: modules, scopes, and Android-specific DI components.

55m
3 problems
Topic Progress 0%

Why Dependency Injection?

The Problem Without DI

Without DI, classes create their own dependencies inside constructors or methods. This tightly couples code — UserViewModel directly instantiates RetrofitUserRepository, which directly creates Retrofit.Builder(). Testing becomes impossible because you can't swap in a fake repository.

What DI Solves

DI inverts this: the class declares what it needs, and an external container (the DI framework) provides it. UserViewModel declares class UserViewModel(private val repository: UserRepository). At runtime, Hilt creates the real RetrofitUserRepository. In tests, you pass a fake.

Hilt vs Manual DI vs Dagger

  • Manual DI: You write a ServiceLocator or pass dependencies through constructors. Works for small projects but becomes boilerplate-heavy.
  • Dagger: Compile-time DI framework for Android. Powerful but verbose — requires @Component, @Module, @Subcomponent with lots of ceremony.
  • Hilt: Built on top of Dagger. Reduces boilerplate with Android-specific annotations (@AndroidEntryPoint, @HiltViewModel). Google's recommended DI solution for Android.

Setup

// build.gradle.kts
plugins {
    id("com.google.dagger.hilt.android")
    id("com.google.devtools.ksp")
}

dependencies {
    implementation("com.google.dagger:hilt-android:2.51")
    ksp("com.google.dagger:hilt-compiler:2.51")
    implementation("androidx.hilt:hilt-navigation-compose:1.2.0")
}

// Application class
@HiltAndroidApp
class MyApplication : Application()

// Activity
@AndroidEntryPoint
class MainActivity : ComponentActivity() {
    // Hilt injects dependencies here
}

Modules and Scopes

Hilt Modules

Modules tell Hilt how to create dependencies. There are two main annotations:

  • @Provides: When you need to call a constructor or builder yourself (e.g., Retrofit, Room).
  • @Binds: When you have an interface and a concrete implementation — Hilt figures out the rest.
@Module
@InstallIn(SingletonComponent::class)
object NetworkModule {

    @Provides
    @Singleton
    fun provideRetrofit(): Retrofit {
        return Retrofit.Builder()
            .baseUrl("https://api.example.com")
            .addConverterFactory(GsonConverterFactory.create())
            .build()
    }

    @Provides
    @Singleton
    fun provideUserApi(retrofit: Retrofit): UserApi {
        return retrofit.create(UserApi::class.java)
    }
}

@Module
@InstallIn(SingletonComponent::class)
abstract class RepositoryModule {

    @Binds
    @Singleton
    abstract fun bindUserRepository(
        impl: RetrofitUserRepository
    ): UserRepository
}

Scopes

Scopes control how long a dependency instance lives:

Scope Component Lifecycle
@Singleton SingletonComponent Entire app
@ActivityScoped ActivityComponent One Activity
@ViewModelScoped ViewModelComponent One ViewModel
@FragmentScoped FragmentComponent One Fragment
No scope ViewComponent Recreated every injection
// Created once for the entire app lifetime
@Singleton
class SessionManager @Inject constructor(
    private val tokenStorage: TokenStorage
)

// Created once per ViewModel
@ViewModelScoped
class CartUseCase @Inject constructor(
    private val cartRepository: CartRepository
)

// Created fresh every time it's injected
class DateFormatter @Inject constructor()

@Inject Constructor

When you own the class, use @Inject constructor directly — no module needed:

@ViewModelScoped
class ProductViewModel @Inject constructor(
    private val getProductUseCase: GetProductUseCase,
    private val addToCartUseCase: AddToCartUseCase
) : ViewModel()

Hilt sees the @Inject constructor, knows how to create all parameters (because they're also provided by Hilt), and creates the ViewModel automatically.

Injecting into Android Components

@AndroidEntryPoint

Annotate Activities, Fragments, Services, and BroadcastReceivers with @AndroidEntryPoint to enable field injection:

@AndroidEntryPoint
class ProductFragment : Fragment() {
    // Hilt injects this before onCreate
    @Inject lateinit var analyticsTracker: AnalyticsTracker

    // ViewModel injected via hiltViewModel()
    private val viewModel: ProductViewModel by viewModels()

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        analyticsTracker.trackScreen("product_detail")
    }
}

ViewModel Injection

Hilt provides a dedicated annotation for ViewModels:

@HiltViewModel
class ProductViewModel @Inject constructor(
    private val getProductUseCase: GetProductUseCase
) : ViewModel() {
    private val _state = MutableStateFlow(ProductUiState())
    val state: StateFlow<ProductUiState> = _state.asStateFlow()

    fun loadProduct(id: String) {
        viewModelScope.launch {
            _state.value = ProductUiState(isLoading = true)
            val product = getProductUseCase(id)
            _state.value = ProductUiState(product = product)
        }
    }
}

In Compose:

@Composable
fun ProductScreen(viewModel: ProductViewModel = hiltViewModel()) {
    val state by viewModel.state.collectAsStateWithLifecycle()
    // render...
}

Testing with Hilt

Swap real implementations for fakes in tests:

@Module
@TestInstallIn(
    components = [SingletonComponent::class],
    replaces = [RepositoryModule::class]
)
abstract class FakeRepositoryModule {
    @Binds
    abstract fun bindUserRepository(
        fake: FakeUserRepository
    ): UserRepository
}

Hilt swaps the module automatically in test builds — no code changes needed in the ViewModel or Activity.

Quiz

1. What is the difference between @Provides and @Binds in Hilt modules?

Question 1 options

2. What does @Singleton scope guarantee in Hilt?

Question 2 options

3. How do you inject a ViewModel in a Composable with Hilt?

Question 3 options

4. What annotation must the Application class have for Hilt to work?

Question 4 options

Flashcards

Question

What does @Inject constructor do in Hilt?

Answer

Tells Hilt how to create the class. Hilt sees all constructor parameters and resolves them from its container automatically — no module needed.

Question

What is the difference between @Singleton and @ViewModelScoped?

Answer

@Singleton creates one instance for the entire app. @ViewModelScoped creates one instance per ViewModel — a new instance is created when the ViewModel is created.

Question

When should you use @Provides vs @Binds?

Answer

@Provides when you need to call a builder or factory yourself (Retrofit, Room). @Binds when mapping an interface to its concrete implementation.

Question

How do you swap real dependencies for fakes in Hilt tests?

Answer

Use @TestInstallIn on a test module that @Binds the interface to a fake implementation. Hilt automatically uses the test module instead of the production module.

Revision Notes

Key Takeaways

  • 1. Hilt reduces Dagger boilerplate with Android-specific annotations like @HiltViewModel and @AndroidEntryPoint
  • 2. @Provides is for building dependencies yourself, @Binds is for interface-to-implementation mapping
  • 3. Scopes control instance lifetime — @Singleton for app-wide, @ViewModelScoped for ViewModel-lifetime
  • 4. Use @TestInstallIn to swap real dependencies for fakes in tests without changing production code

Interview Tips

  • Explain DI as inverting control — the class declares what it needs, the container provides it
  • Discuss why you'd choose Hilt over manual DI or Koin for larger projects
  • Know the difference between @Provides and @Binds and when to use each
  • Be ready to explain how testing works with Hilt by swapping modules

Cheat Sheet

Hilt DI Cheat Sheet

Setup:

@HiltAndroidApp
class MyApp : Application()

@AndroidEntryPoint
class MainActivity : ComponentActivity()

Modules:

  • @Provides: Call constructor/builder yourself
  • @Binds: Map interface → implementation
  • @InstallIn(SingletonComponent::class) for app-wide

Scopes:

Annotation Lifetime
@Singleton Entire app
@ActivityScoped One Activity
@ViewModelScoped One ViewModel

ViewModel Injection:

@HiltViewModel
class MyVM @Inject constructor(
    private val repo: Repository
) : ViewModel()

// In Composable:
val vm: MyVM = hiltViewModel()

Testing:

@Module
@TestInstallIn(...)
abstract class FakeModule {
    @Binds abstract fun bind(repo: FakeRepo): Repository
}