Dependency injection is a design pattern where a class receives the objects it needs from outside rather than creating them itself with hard coded constructors inside its own code. That simple change makes code more flexible because you can swap an implementation for testing or for a different environment without editing the class that uses it. Hilt builds on Dagger and provides standard Android components and entry points that reduce the boilerplate that pure Dagger requires for activities and fragments and view models. Together they help teams build apps where dependencies are explicit and testable and where object creation lives in modules that are easy to review. This guide explains the core ideas and the practical steps to use Hilt in a real Android project.
Without injection a class often creates its own database and network client and repository inside its constructor or inside its methods and that hidden creation makes testing painful. A test then needs a real database and a real server instead of a simple fake that returns controlled data for a specific scenario. With injection the class declares its needs as constructor parameters and a test can pass fakes directly without any framework magic or complex setup. Production code benefits too because changing a network implementation affects only the module that provides it rather than every class that used the old implementation directly. Injection therefore improves both testability and maintainability in a way that grows more valuable as the app grows larger.
Core Concepts Without Jargon
A dependency is simply an object that another object needs to do its work such as a repository that a view model needs to load data for a screen. Injection means supplying that object from outside usually through a constructor so the receiving class does not decide which concrete implementation to create. A module is a place where you define how to create objects that cannot be constructed automatically such as objects from external libraries or objects that need configuration values. A scope controls how long an object lives and whether different consumers share the same instance or receive a fresh instance for each request. Scopes in Android often follow component lifetimes such as a single activity or a single application so shared objects match the lifetime of the screen or app that uses them.
Components are containers that know how to build a graph of dependencies and Hilt generates standard components for the application and for activities and fragments and view models. An entry point is a place where Hilt can inject dependencies into a class that the framework creates such as an activity that the system instantiates rather than your code. Annotated fields in such classes receive their dependencies after creation through a process that Hilt manages automatically once the class is marked correctly. Understanding these terms is enough to start because most daily Hilt work involves marking classes and writing small modules rather than building custom components from scratch. Keep the graph simple at first and add complexity only when a real need appears in the project.
- Pass dependencies through constructors so needs are explicit and easy to see.
- Use modules only for objects that need manual creation or configuration values.
- Match scopes to lifetimes so shared objects do not outlive their owner screen.
- Prefer a single instance for stateless services and repositories shared widely.
- Keep the dependency graph simple until a real complexity need appears.
Setting Up Hilt in a Project
Setup begins with plugins and dependencies in Gradle files that enable annotation processing and code generation for Hilt components in your app module. The application class needs a Hilt annotation so Hilt can create the application component that lives for the whole app lifetime and serves as the root of the graph. Activities and fragments that need injection need their own annotations so Hilt can supply field dependencies after the system creates those classes. View models can receive dependencies through a special annotation on their constructor and Hilt will supply them when the view model is created for a screen. After setup a simple build verifies that code generation works and that the application class is correctly registered in the manifest for the app.
Common setup mistakes include forgetting the application annotation or forgetting to register the application class in the manifest so Hilt cannot find its root component at runtime. Another mistake is annotating a fragment but not its host activity when the fragment expects dependencies from an activity scoped component that was never created. Read the first build error carefully because Hilt errors usually name the missing binding or the class that lacks an annotation in a clear way. Fix one error at a time and rebuild so you can see whether the next error is related or independent from the first problem. A clean setup phase makes later feature work smooth because new classes can simply declare dependencies and receive them without extra wiring.
Modules Scopes and Qualifiers
Modules provide objects that Hilt cannot create automatically such as a network client that needs a base URL and timeouts and interceptors configured in a specific way. A module function returns an instance and annotations declare its scope and its lifetime so Hilt knows when to reuse it and when to create a new one. Singleton scope suits objects that are expensive to create and safe to share such as a database instance or a network client used across many screens. Activity scope suits objects tied to one screen flow such as a navigator that should not survive after the activity finishes and releases its resources. Choosing the wrong scope can cause leaks when a long lived object holds a short lived screen reference so scope decisions deserve careful thought during code review.
Qualifiers distinguish between two objects of the same type when an app needs both such as two different string values for two different base URLs or two different dispatchers for background work. A qualifier annotation on a provider function and on an injection site tells Hilt exactly which instance to supply for that particular need without ambiguity. Use qualifiers sparingly because too many similar bindings make the graph hard to understand and increase the chance of wiring the wrong instance into a class. Prefer distinct types or wrapper classes when two objects play very different roles in the app and reserve qualifiers for genuine cases of same type different value. Clear naming for qualifiers also helps future developers understand why two bindings exist for one type in the same module.
Testing With Injected Dependencies
Testing becomes easier with injection because a view model test can pass a fake repository that returns fixed data for a screen without a database or a network call. A fake lets you test loading states and error states and empty states by controlling exactly what the repository returns for each test case. For interface tests you can replace a module binding with a test binding that supplies a fake implementation for the whole test application in a controlled way. That replacement keeps tests fast and reliable because they no longer depend on server availability or network speed during a continuous integration run. Teams that invest in test doubles early find that feature changes become less risky because tests catch regressions before release.
End to end tests still have value for critical flows but they should not be the only tests for logic that unit tests with fakes can verify faster and more precisely. Keep production modules and test modules separate so test bindings never leak into a release build through an accidental configuration change. You can learn broader testing strategy in testing Android apps with unit and Espresso tests alongside injection specific test patterns. You can also study repository design in Room database for local SQLite storage because repositories are common injection targets in Android apps. A test friendly architecture is one of the strongest practical arguments for adopting injection in a growing project.
Practical Patterns and Pitfalls
Avoid injecting into every small class just because the framework allows it because over injection can hide simple creation logic behind a graph that is harder to follow than a direct constructor call. Use injection for objects with real dependencies and for objects that benefit from sharing or testing while leaving simple value objects as plain constructors. Avoid field injection in classes you create yourself because constructor injection makes dependencies visible and makes tests straightforward without framework setup. Avoid holding activity references in singleton objects because that mistake leaks an entire screen and its views long after the user has navigated away. Review scopes during code review and ask whether each shared object truly needs to live as long as its scope allows.
Migration to Hilt can happen gradually because you can introduce it for new features while older code continues to create its dependencies manually until it is refactored. Start with a view model and its repository for a new screen so the team learns Hilt patterns on a contained feature before applying them across the app. Document module responsibilities so developers know where to add a new binding instead of creating duplicate providers in several modules. You can review project organisation in the guide to Android project structure when deciding where modules belong in your codebase. With disciplined use Hilt reduces boilerplate and improves testability without turning a simple app into a complex framework exercise.
A final habit keeps injection simple because you should review each new module binding and ask whether a plain constructor would serve the same purpose with less framework machinery. That question prevents over injection in small classes where direct creation is clearer and easier to follow for new developers. Document the reason for each scope choice in a short comment so reviewers can see why a shared object lives as long as its scope allows. With that discipline Hilt stays a helpful tool for testability rather than a complex graph that obscures simple object creation in the app.
@Module
@InstallIn(SingletonComponent::class)
object NetworkModule {
@Provides
@Singleton
fun provideApi(): ApiService {
return Retrofit.Builder().baseUrl(BASE_URL).build()
.create(ApiService::class.java)
}
}
The steps above follow the approach described in the official developer documentation for this platform and reflect common practice used by experienced developers when they build and test Android apps in a careful and organised way.


