On this page
Dependency injection (DI) means objects receive their dependencies instead of creating them. A ViewModel that receives a WeatherRepository can be given a fake in tests and the real one in the app. Hilt automates the wiring and manages object lifetimes tied to Android components.
Why DI, and why Hilt
class WeatherViewModel : ViewModel() {
private val repository = WeatherRepositoryImpl(
Retrofit.Builder().baseUrl(BASE_URL).build().create(WeatherApi::class.java),
Room.databaseBuilder(/* needs a Context... */).build().weatherDao(),
)
}@HiltViewModel
class WeatherViewModel @Inject constructor(
private val repository: WeatherRepository,
) : ViewModel()Hilt generates the Dagger code at compile time, so wiring errors are caught at build time and there's no reflection at runtime. It also understands Android lifecycles: Application, Activity, ViewModel, Fragment and Service.
Setup
plugins {
id("com.google.devtools.ksp")
id("com.google.dagger.hilt.android")
}
dependencies {
implementation("com.google.dagger:hilt-android:<hilt-version>")
ksp("com.google.dagger:hilt-compiler:<hilt-version>")
implementation("androidx.hilt:hilt-navigation-compose:<version>") // hiltViewModel() in Compose
}@HiltAndroidApp
class App : Application()
@AndroidEntryPoint
class MainActivity : ComponentActivity() { /* ... */ }How Hilt builds the graph
Modules: teaching Hilt to create things
Classes you own get @Inject constructor. For interfaces and third-party classes, write modules:
@Module
@InstallIn(SingletonComponent::class)
object NetworkModule {
@Provides @Singleton
fun provideOkHttp(): OkHttpClient = OkHttpClient.Builder()
.connectTimeout(15, TimeUnit.SECONDS)
.build()
@Provides @Singleton
fun provideRetrofit(client: OkHttpClient, json: Json): Retrofit = Retrofit.Builder()
.baseUrl("https://api.example.com/")
.client(client)
.addConverterFactory(json.asConverterFactory("application/json".toMediaType()))
.build()
@Provides @Singleton
fun provideJson(): Json = Json { ignoreUnknownKeys = true }
@Provides
fun provideWeatherApi(retrofit: Retrofit): WeatherApi = retrofit.create(WeatherApi::class.java)
}
@Module
@InstallIn(SingletonComponent::class)
abstract class RepositoryModule {
@Binds @Singleton
abstract fun bindWeatherRepository(impl: DefaultWeatherRepository): WeatherRepository
}Use @Binds to map an interface to an implementation that has an @Inject constructor. It's less code and more efficient. Use @Provides when you must call builders or factories.
Qualifiers: two of the same type
@Qualifier @Retention(AnnotationRetention.BINARY) annotation class IoDispatcher
@Qualifier @Retention(AnnotationRetention.BINARY) annotation class DefaultDispatcher
@Module @InstallIn(SingletonComponent::class)
object DispatchersModule {
@Provides @IoDispatcher fun io(): CoroutineDispatcher = Dispatchers.IO
@Provides @DefaultDispatcher fun default(): CoroutineDispatcher = Dispatchers.Default
}
class ImageRepository @Inject constructor(@IoDispatcher private val io: CoroutineDispatcher)ViewModels in Compose
@Composable
fun WeatherRoute(viewModel: WeatherViewModel = hiltViewModel()) { /* ... */ }For runtime arguments that aren't in SavedStateHandle, Hilt supports assisted injection for ViewModels with @AssistedInject and an assisted factory. Check the Hilt docs for the current Compose API.
Testing
@UninstallModules(RepositoryModule::class)
@HiltAndroidTest
class WeatherScreenTest {
@get:Rule(order = 0) val hilt = HiltAndroidRule(this)
@get:Rule(order = 1) val compose = createAndroidComposeRule<MainActivity>()
@BindValue @JvmField val repo: WeatherRepository = FakeWeatherRepository()
@Test fun showsForecast() { /* ... */ }
}For plain unit tests you often don't need Hilt at all: construct the ViewModel directly with fakes. That's the main benefit of constructor injection.
Practical guidance
Small app
Hilt with a few modules: network, database, repositories. Most classes use @Inject constructor.
Multi-module app
Each module contributes its own Hilt modules, and the :app module assembles the graph.
KMP or lighter needs
Consider Koin or manual DI. Hilt is Android-only and annotation-based.
Common mistakes
- Scoping everything
@Singleton, which keeps objects alive forever. Scope only what must be shared. - Injecting Activity Context into singletons, which leaks. Use
@ApplicationContext. - Field injection everywhere. Prefer constructor injection, and use field injection only in Android classes Hilt instantiates.
- Huge modules providing everything. Split them by feature or layer.
Frequently asked questions
Hilt or Koin?
Hilt does compile-time validation and is Google's recommendation for Android. Koin is a lightweight runtime DSL that works on Kotlin Multiplatform. Both are viable, so choose based on platform and team preference.
Does Hilt slow down builds?
Annotation processing adds build time. Using KSP instead of kapt helps, and modularisation limits recompilation.
Is manual DI ever better?
For very small apps or libraries, a simple manual container can be enough. Hilt pays off as the graph grows.
Conclusion
Hilt gives Android apps compile-time-checked dependency injection with lifecycle-aware scopes. Use constructor injection, @Binds for interfaces, @Provides for builders, qualifiers for duplicate types, and the shortest sensible scope.
Next step: decide where business logic lives in Use Cases and Domain Layers.
Official references
π± Apps built by Yunok Tech
Real Android apps on Google Play, free to download.
Walzi4K wallpapers for lock and home screenGet it βΈ
NotiVaultNotification history: recover dismissed notificationsGet it βΈ
Ready to build real Android applications?
Architecture makes the most sense when you apply it to a real project and see why each boundary exists. Learn how maintainable Android applications are designed by building one.
Move beyond theory and strengthen your Android development skills with Yunoktech's Android course. Learn core concepts, practical implementation, modern Android development techniques, and the reasoning behind building maintainable applications.
Explore the Android course and take your next step as an Android developer.