Dependency Injection in Android with Hilt

By Christian Torrico  ·  April 28, 2026

Android Hilt Dependency Injection Kotlin
Dependency Injection in Android with Hilt

Dependency Injection (DI) is one of those concepts that sounds intimidating but becomes second nature once you understand the core idea: instead of creating your own dependencies, you receive them from the outside. Hilt is Google’s opinionated DI library for Android, built on top of Dagger 2 but with significantly less boilerplate.

Why Dependency Injection?

Imagine a UserRepository that needs a UserApi and a UserDao. Without DI:

class UserRepository {
    private val api = UserApi()           // hard-coded dependency
    private val dao = UserDao(database)   // tightly coupled
}

This is hard to test (how do you mock UserApi?), hard to configure (what if you need a different API in production vs testing?), and violates the Single Responsibility Principle.

With DI:

class UserRepository(
    private val api: UserApi,
    private val dao: UserDao
) { ... }
// Dependencies are injected — easy to test, easy to swap

Setting Up Hilt

Add to your build.gradle.kts:

// In project-level build.gradle
plugins {
    id("com.google.dagger.hilt.android") version "2.51" apply false
}

// In app-level build.gradle
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")
}

Annotate your Application class:

@HiltAndroidApp
class MyApp : Application()

That’s it for setup.

Providing Dependencies

Hilt needs to know how to create each dependency. For classes you own, use @Inject constructor:

class UserRepository @Inject constructor(
    private val api: UserApi,
    private val dao: UserDao
) {
    suspend fun getUser(id: String) = api.fetchUser(id)
}

For third-party classes or interfaces, use @Module with @Provides or @Binds:

@Module
@InstallIn(SingletonComponent::class)
object NetworkModule {

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

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

// Use @Binds to bind an interface to an implementation
@Module
@InstallIn(SingletonComponent::class)
abstract class RepositoryModule {

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

Scopes

Scopes control how long an instance lives:

ScopeComponentLifetime
@SingletonSingletonComponentApp lifetime
@ActivityScopedActivityComponentActivity lifetime
@ViewModelScopedViewModelComponentViewModel lifetime
@FragmentScopedFragmentComponentFragment lifetime

Always match the scope to the minimum required lifetime.

Injecting into ViewModels

Use @HiltViewModel to inject into ViewModels:

@HiltViewModel
class UserViewModel @Inject constructor(
    private val getUserProfile: GetUserProfileUseCase,
    private val updateProfile: UpdateProfileUseCase
) : ViewModel() {

    private val _profile = MutableStateFlow<UserProfile?>(null)
    val profile = _profile.asStateFlow()

    fun loadProfile(userId: String) {
        viewModelScope.launch {
            getUserProfile(userId).onSuccess { _profile.value = it }
        }
    }
}

In your Compose UI, just use hiltViewModel():

@Composable
fun ProfileScreen(
    viewModel: UserViewModel = hiltViewModel()
) {
    val profile by viewModel.profile.collectAsStateWithLifecycle()
    // ...
}

Testing with Hilt

One of Hilt’s best features is how easy it makes testing. Replace real implementations with fakes:

@HiltAndroidTest
class UserRepositoryTest {

    @get:Rule
    val hiltRule = HiltAndroidRule(this)

    @Inject
    lateinit var userRepository: UserRepository

    @Before
    fun init() {
        hiltRule.inject()
    }

    @Test
    fun `getUser returns user when API succeeds`() = runTest {
        val result = userRepository.getUser("123")
        assertTrue(result.isSuccess)
    }
}

// In test module, replace network with fake
@Module
@TestInstallIn(
    components = [SingletonComponent::class],
    replaces = [NetworkModule::class]
)
object FakeNetworkModule {
    @Provides
    @Singleton
    fun provideFakeUserApi(): UserApi = FakeUserApi()
}

Hilt eliminates so much boilerplate compared to raw Dagger that there’s simply no reason not to use it on any new Android project. The investment in understanding DI pays dividends throughout the entire development lifecycle.

Back to all posts