Dependency Injection with Hilt/Dagger

📖 Concept

Dependency Injection (DI) is mandatory knowledge for senior Android developers. Hilt (built on Dagger) is Google's recommended DI framework for Android.

Why DI matters:

  1. Testability — Replace real implementations with fakes/mocks in tests
  2. Decoupling — Classes depend on interfaces, not concrete implementations
  3. Lifecycle management — Hilt scopes instances to Android lifecycle (Activity, ViewModel, etc.)
  4. Code reuse — Same interface, different implementations per build variant

Hilt component hierarchy:

SingletonComponent (Application scope)
  ├── ActivityRetainedComponent (survives config change)
  │   ├── ViewModelComponent (ViewModel scope)
  │   └── ActivityComponent (Activity scope)
  │       ├── FragmentComponent  
  │       └── ViewComponent
  └── ServiceComponent (Service scope)

Key Hilt annotations:

  • @HiltAndroidApp — Application class, generates the root component
  • @AndroidEntryPoint — Activity/Fragment/Service, enables injection
  • @HiltViewModel — ViewModel, enables constructor injection
  • @Inject — Marks a constructor or field for injection
  • @Module / @Provides — Provides instances that can't be constructor-injected
  • @Binds — Binds an interface to its implementation (more efficient than @Provides)
  • @Singleton / @ActivityScoped — Scoping annotations

Under the hood: Dagger generates code at compile time (no reflection). For each @Inject constructor, Dagger creates a Factory class. For each @Module, it creates a provider. The component wires everything together.

💻 Code Example

codeTap to expand ⛶
1// Application setup
2@HiltAndroidApp
3class MyApplication : Application()
4
5// Module — providing dependencies
6@Module
7@InstallIn(SingletonComponent::class)
8abstract class DataModule {
9 @Binds
10 @Singleton
11 abstract fun bindTaskRepository(impl: TaskRepositoryImpl): TaskRepository
12}
13
14@Module
15@InstallIn(SingletonComponent::class)
16object NetworkModule {
17 @Provides
18 @Singleton
19 fun provideOkHttpClient(): OkHttpClient {
20 return OkHttpClient.Builder()
21 .addInterceptor(AuthInterceptor())
22 .addInterceptor(HttpLoggingInterceptor().apply {
23 level = HttpLoggingInterceptor.Level.BODY
24 })
25 .connectTimeout(30, TimeUnit.SECONDS)
26 .build()
27 }
28
29 @Provides
30 @Singleton
31 fun provideRetrofit(client: OkHttpClient): Retrofit {
32 return Retrofit.Builder()
33 .baseUrl("https://api.example.com/")
34 .client(client)
35 .addConverterFactory(MoshiConverterFactory.create())
36 .build()
37 }
38
39 @Provides
40 @Singleton
41 fun provideTaskApi(retrofit: Retrofit): TaskApi {
42 return retrofit.create(TaskApi::class.java)
43 }
44}
45
46// Repository with constructor injection
47class TaskRepositoryImpl @Inject constructor(
48 private val taskApi: TaskApi,
49 private val taskDao: TaskDao,
50 @IoDispatcher private val ioDispatcher: CoroutineDispatcher
51) : TaskRepository {
52 override fun getTasks(): Flow<List<Task>> = taskDao.getAll()
53}
54
55// ViewModel with Hilt
56@HiltViewModel
57class TaskViewModel @Inject constructor(
58 private val getTasksUseCase: GetTasksUseCase,
59 private val savedStateHandle: SavedStateHandle
60) : ViewModel() {
61 // savedStateHandle is provided automatically by Hilt
62 private val filter = savedStateHandle.getStateFlow("filter", TaskFilter.ALL)
63}
64
65// Qualifier for multiple bindings of same type
66@Qualifier
67@Retention(AnnotationRetention.BINARY)
68annotation class IoDispatcher
69
70@Qualifier
71@Retention(AnnotationRetention.BINARY)
72annotation class MainDispatcher
73
74@Module
75@InstallIn(SingletonComponent::class)
76object DispatcherModule {
77 @Provides @IoDispatcher
78 fun provideIoDispatcher(): CoroutineDispatcher = Dispatchers.IO
79
80 @Provides @MainDispatcher
81 fun provideMainDispatcher(): CoroutineDispatcher = Dispatchers.Main
82}

🏋️ Practice Exercise

Practice:

  1. Set up Hilt in a new project with proper Application, Activity, and ViewModel injection
  2. Create a @Module with both @Provides and @Binds — when do you use each?
  3. Implement a Qualifier for test vs production API URLs
  4. Explain the Hilt component hierarchy and scoping rules
  5. Write a unit test that replaces a Hilt module with a test module using @TestInstallIn

⚠️ Common Mistakes

  • Using @Provides when @Binds suffices — @Binds is more efficient (no method body, directly maps interface to impl)

  • Not scoping correctly — @Singleton in a ViewModelComponent module means the instance outlives the ViewModel

  • Injecting Activity context where Application context is needed — causes memory leaks

  • Circular dependencies — A depends on B, B depends on A. Use @Lazy or restructure

  • Forgetting @AndroidEntryPoint on Activities/Fragments — injection silently fails

💼 Interview Questions

🎤 Mock Interview

Mock interview is powered by AI for Dependency Injection with Hilt/Dagger. Login to unlock this feature.