Skip to content
All posts
July 14, 20264 min read

Hilt Dependency Injection: Everything You Need to Know

A from-the-ground-up guide to Hilt on Android — what dependency injection actually buys you, the core annotations, how to provide and scope dependencies, and the small set of rules that keep DI from turning into a maze.

HiltDependency InjectionAndroidKotlinArchitecture
Share:

Dependency injection sounds like an enterprise word for a simple idea: instead of an object creating the things it depends on, those things are handed to it. Hilt is Google's standard way to do that on Android, built on top of Dagger but with most of the boilerplate removed. If you've avoided it because Dagger scared you, Hilt is worth a fresh look — it does the wiring so you can spend your attention on the app.

Why Bother With DI At All

The honest answer is testability and decoupling. When a

code
ViewModel
creates its own repository, which creates its own network client, you can't swap any of it in a test without reaching into the implementation. When those dependencies are injected, you hand the ViewModel a fake repository in a test and a real one in production, with no change to the ViewModel itself. That single property — being able to replace a dependency from the outside — is what makes a codebase testable, and testability is what lets you change code later without fear.

The second benefit is that object creation stops being scattered everywhere. Without DI, the knowledge of how to build a repository lives in every place that builds one. With Hilt, it lives in one module, and everyone asks for the finished object. As an app grows, that centralization is the difference between a wiring diagram you can follow and a tangle you can't.

The Core Annotations

Hilt's surface is small once you see the pattern. You mark your

code
Application
so Hilt can generate the dependency container:

kotlin
@HiltAndroidApp
class MyApp : Application()

You mark the Android entry points that need injection:

kotlin
@AndroidEntryPoint
class MainActivity : ComponentActivity() { /* ... */ }

And you mark a ViewModel so Hilt provides its constructor dependencies:

kotlin
@HiltViewModel
class HomeViewModel @Inject constructor(
    private val repository: UserRepository,
) : ViewModel()

The

code
@Inject constructor
is the heart of it. Hilt reads the constructor, figures out what the class needs, and supplies it — recursively, all the way down.

Providing Things Hilt Can't Construct

Hilt can build any class with an

code
@Inject
constructor automatically. It can't build interfaces, or types you don't own (like a Retrofit instance). For those you write a module.

kotlin
@Module
@InstallIn(SingletonComponent::class)
object NetworkModule {
    @Provides
    @Singleton
    fun provideApi(): UserApi = Retrofit.Builder()
        .baseUrl("https://api.example.com/")
        .build()
        .create(UserApi::class.java)
}

For binding an interface to its implementation,

code
@Binds
is leaner than
code
@Provides
because it generates less code:

kotlin
@Module
@InstallIn(SingletonComponent::class)
abstract class RepoModule {
    @Binds
    abstract fun bindUserRepo(impl: UserRepositoryImpl): UserRepository
}

Scopes Decide Lifetime

A scope answers "how long does this object live and who shares it."

code
@Singleton
lives for the whole app.
code
@ActivityRetainedScoped
survives configuration changes.
code
@ViewModelScoped
lives as long as one ViewModel. Picking the right scope matters: scope something too widely and it holds memory it shouldn't; scope it too narrowly and you rebuild it more than necessary. The default I reach for is the application scope for stateless singletons like a network client, and narrower scopes only when an object genuinely holds per-screen state.

The Rules That Keep It Simple

Hilt only becomes a maze if you let it. Three habits keep mine readable. First, prefer constructor injection everywhere; field injection is only for framework classes you don't construct yourself. Second, keep modules small and named for what they provide — a

code
NetworkModule
, a
code
DatabaseModule
— rather than one giant
code
AppModule
. Third, don't create a scope until you can name the state it protects; most objects are happy as unscoped or singletons, and inventing scopes "for flexibility" is exactly the speculative complexity that makes DI feel heavy. Follow those and Hilt stays the quiet wiring layer it's supposed to be.

One last thing worth internalizing: Hilt rewards consistency more than cleverness. Across many apps, the value isn't any single advanced feature — it's that every project wires its dependencies the same predictable way, so moving between codebases costs nothing. When a new developer (or a future you) opens the project, the dependency graph is discoverable from the annotations rather than buried in hand-written factories. That predictability is the real return on adopting a DI framework, and it only holds if you keep using the simple, conventional patterns instead of inventing bespoke wiring for each app. Boring and uniform is exactly what you want from the layer that holds everything else together.

Key Takeaways

  • DI's real payoff is testability — injected dependencies can be swapped for fakes from the outside without touching the class.
  • The core is
    code
    @HiltAndroidApp
    ,
    code
    @AndroidEntryPoint
    ,
    code
    @HiltViewModel
    , and
    code
    @Inject constructor
    ; Hilt resolves the rest recursively.
  • Write modules only for interfaces and types you don't own; use
    code
    @Binds
    for interface-to-impl and
    code
    @Provides
    for constructed objects.
  • Choose scopes by lifetime and sharing; default to singletons for stateless services and narrow scopes only for per-screen state.
  • Prefer constructor injection, keep modules small and purpose-named, and don't invent scopes without a concrete reason.
Share:
S

Sudarshan Chaudhari

AI Systems Builder / Product Engineer

Bangkok, Thailand

Solo Android developer with 13+ years in QA, building Android apps, AI automation systems, and developer tools at SudarshanTechLabs.

Stay updated

Get new posts on Android, Kotlin, and solo dev straight to your inbox.

Newsletter preferences

Related Apps

MyFamilyTracker

Real-time family location sharing — Firebase Realtime DB for sub-second propagation, WorkManager + ForegroundService for OS-compliant background collection, geofencing via Google Maps API.

Building something? Available for Android dev and QA consulting.

Work with me

Comments — powered by Giscus

Apps tagged with this