Why Dependency Injection Matters
Setting Up Hilt
// build.gradle.kts (project level)
plugins {
id("com.google.dagger.hilt.android") version "2.51" apply false
id("com.google.devtools.ksp") version "2.0.0-1.0.22" apply false
}
// build.gradle.kts (app level)
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-android-compiler:2.51")
// For ViewModel injection
implementation("androidx.hilt:hilt-navigation-compose:1.2.0")
// Testing
testImplementation("com.google.dagger:hilt-android-testing:2.51")
kspTest("com.google.dagger:hilt-android-compiler:2.51")
}
// Application class
@HiltAndroidApp
class MyApplication : Application()Modules: Telling Hilt How to Provide Dependencies
// Binding interfaces to implementations
@Module
@InstallIn(SingletonComponent::class)
abstract class RepositoryModule {
@Binds
@Singleton
abstract fun bindUserRepository(
impl: UserRepositoryImpl
): UserRepository
@Binds
@Singleton
abstract fun bindAnalyticsService(
impl: FirebaseAnalyticsService
): AnalyticsService
}
// Providing third-party objects
@Module
@InstallIn(SingletonComponent::class)
object NetworkModule {
@Provides
@Singleton
fun provideOkHttpClient(): OkHttpClient {
return OkHttpClient.Builder()
.connectTimeout(30, TimeUnit.SECONDS)
.readTimeout(30, TimeUnit.SECONDS)
.addInterceptor(HttpLoggingInterceptor().apply {
level = if (BuildConfig.DEBUG)
HttpLoggingInterceptor.Level.BODY
else HttpLoggingInterceptor.Level.NONE
})
.build()
}
@Provides
@Singleton
fun provideRetrofit(client: OkHttpClient): Retrofit {
return Retrofit.Builder()
.baseUrl("https://api.example.com/")
.client(client)
.addConverterFactory(
Json.asConverterFactory(
"application/json".toMediaType()
)
)
.build()
}
@Provides
@Singleton
fun provideApiService(retrofit: Retrofit): ApiService {
return retrofit.create(ApiService::class.java)
}
}Scopes and Component Hierarchy
// Singleton: one instance for the entire app
@Singleton
class AppDatabase @Inject constructor(
@ApplicationContext context: Context
) {
val db = Room.databaseBuilder(
context, AppDb::class.java, "app.db"
).build()
}
// ViewModelScoped: one per ViewModel instance
@ViewModelScoped
class GetUserProfileUseCase @Inject constructor(
private val userRepo: UserRepository,
private val settingsRepo: SettingsRepository
) {
suspend operator fun invoke(userId: String): UserProfile {
val user = userRepo.getUser(userId)
val settings = settingsRepo.getUserSettings(userId)
return UserProfile(user, settings)
}
}
// ViewModel: inject use cases, not raw repositories
@HiltViewModel
class ProfileViewModel @Inject constructor(
private val getUserProfile: GetUserProfileUseCase,
private val savedStateHandle: SavedStateHandle
) : ViewModel() {
private val userId = savedStateHandle.get<String>("userId")!!
val profile = flow {
emit(getUserProfile(userId))
}.stateIn(viewModelScope, SharingStarted.Lazily, null)
}Qualifiers: Multiple Bindings of the Same Type
// Define qualifiers
@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class IoDispatcher
@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class DefaultDispatcher
// Provide qualified instances
@Module
@InstallIn(SingletonComponent::class)
object DispatcherModule {
@IoDispatcher
@Provides
fun provideIoDispatcher(): CoroutineDispatcher =
Dispatchers.IO
@DefaultDispatcher
@Provides
fun provideDefaultDispatcher(): CoroutineDispatcher =
Dispatchers.Default
}
// Inject the specific dispatcher you need
class UserRepositoryImpl @Inject constructor(
private val api: ApiService,
private val dao: UserDao,
@IoDispatcher private val ioDispatcher: CoroutineDispatcher
) : UserRepository {
override suspend fun getUser(id: String): User =
withContext(ioDispatcher) {
dao.getUser(id) ?: api.fetchUser(id).also {
dao.insertUser(it)
}
}
}Testing with Hilt
// Unit test: no Hilt needed, just pass fakes
class ProfileViewModelTest {
private val fakeUserRepo = FakeUserRepository()
private val fakeSettingsRepo = FakeSettingsRepository()
private val useCase = GetUserProfileUseCase(
fakeUserRepo, fakeSettingsRepo
)
@Test
fun `profile loads successfully`() = runTest {
fakeUserRepo.addUser(User("1", "Jane"))
fakeSettingsRepo.addSettings(
Settings("1", darkMode = true)
)
val viewModel = ProfileViewModel(
getUserProfile = useCase,
savedStateHandle = SavedStateHandle(
mapOf("userId" to "1")
)
)
val profile = viewModel.profile.first()
assertEquals("Jane", profile?.user?.name)
}
}
// Integration test: replace modules with test versions
@HiltAndroidTest
@UninstallModules(NetworkModule::class)
class ProfileScreenTest {
@get:Rule
val hiltRule = HiltAndroidRule(this)
@BindValue
val fakeApi: ApiService = FakeApiService()
@Before
fun setup() {
hiltRule.inject()
}
@Test
fun profileDisplaysUserName() {
// fakeApi is injected everywhere ApiService is used
(fakeApi as FakeApiService).setUser(
User("1", "Jane Doe")
)
// ... launch composable and assert
}
}Key Takeaways
- 1Hilt removes Dagger boilerplate with Android-specific components and scopes.
- 2Use @Binds for interface-to-implementation mappings, @Provides for complex construction.
- 3Scope wisely: @Singleton for global objects, @ViewModelScoped for screen-specific use cases.
- 4Qualifiers disambiguate multiple bindings of the same type (dispatchers, URLs).
- 5Constructor injection makes classes testable without Hilt -- only the wiring needs DI.
- 6Use @UninstallModules and @BindValue to swap real dependencies for fakes in integration tests.
Frequently Asked
Should I use Hilt or manual DI?
Use Hilt for production apps. It provides standard components, reduces boilerplate, and integrates with Android lifecycle. Manual DI is only reasonable for very small apps.
How do I inject into custom views?
Use @AndroidEntryPoint on the containing Activity/Fragment and pass dependencies to the view. Alternatively, use Hilt's @CustomComponent for custom injection.
Ready to architect your next Android app?
ANDROID-ARCHITECT generates production-ready Kotlin code, architecture blueprints, and CI/CD configurations from plain-language descriptions. Start building for free.