Why Dependency Injection Matters in Android
Decoupling. It allows you to swap a RealDatabase for a FakeDatabase in tests instantly. It manages object lifecycles for you. Dependency Injection is fundamental to modern Android architecture.
Benefits of DI:
- Testability: Replace dependencies with mocks in tests
- Flexibility: Swap implementations without changing consuming code
- Lifecycle Management: Automatic cleanup when components are destroyed
- Code Reuse: Share instances across components
- Maintainability: Clear dependencies make code easier to understand
Without DI, you'd need to manually create dependencies, manage their lifecycles, and modify code to swap implementations. With DI, Hilt handles all of this automatically.
Dependency Injection is not optional for modern Android apps—it's essential for testability and maintainability.
Hilt vs Dagger: Understanding the Evolution
Hilt is built on Dagger. It provides standard containers (Application, Activity, Fragment) so you don't have to define complex Components manually. Hilt is Google's opinionated wrapper around Dagger that simplifies Android DI:
Hilt advantages:
- Less Boilerplate: Pre-configured components for Android
- Standard Scopes: Built-in scopes for Application, Activity, Fragment
- Automatic Integration: Works out of the box with Android lifecycle
- Easier Setup: Less configuration than raw Dagger
- Code Generation: Compile-time safety with generated code
When to use Hilt vs Dagger:
- Use Hilt: For Android apps—it's purpose-built for Android
- Use Dagger: For non-Android projects or custom component hierarchies
If you're building an Android app, Hilt is the clear choice. It provides all of Dagger's power with Android-specific conveniences that save significant development time.
Setup and Annotation: Getting Started with Hilt
Add @HiltAndroidApp to your Application class. Add @AndroidEntryPoint to Activities/Fragments. Hilt creates the graph automatically. The setup is remarkably simple:
Step 1: Application Class
- Add
@HiltAndroidAppannotation to your Application class - This generates the dependency injection component for your app
- Hilt handles all the code generation automatically
Step 2: Android Components
- Add
@AndroidEntryPointto Activities, Fragments, Views, Services - This enables dependency injection in these components
- Hilt automatically provides dependencies through @Inject
Step 3: Inject Dependencies
- Use
@Injecton constructor parameters - Hilt automatically provides instances
- Works for your classes and third-party libraries (via Modules)
That's it! Hilt handles the rest through compile-time code generation. No manual Component creation or Provider setup needed.
Modules and Provides: Integrating Third-Party Libraries
For classes you don't own (Retrofit, Room), use Modules. Annotate functions with @Provides to tell Hilt how to build them. Modules bridge Hilt with libraries you can't modify:
Creating Modules:
- @Module: Marks a class as a Hilt module
- @InstallIn: Specifies where module is available (ApplicationComponent, ActivityComponent, etc.)
- @Provides: Marks a function as a dependency provider
- @Singleton: Ensures one instance for app lifetime
Common Module Patterns:
- Retrofit Module: Provides API service instances
- Database Module: Provides Room database instance
- Repository Module: Provides repository implementations
- Preferences Module: Provides SharedPreferences instance
Modules are where you configure third-party libraries. Hilt uses these providers to create instances when needed. This separation keeps your code clean and testable.
ViewModel Injection: Simplifying ViewModels
Add @HiltViewModel and @Inject constructor. No more ViewModelFactories! Hilt handles the factory creation internally. This is one of Hilt's biggest quality-of-life improvements:
Before Hilt (Manual Factory):
- Create ViewModelFactory for each ViewModel
- Manually pass dependencies to factory
- Use factory in Activity/Fragment
- Significant boilerplate code
With Hilt:
- Add
@HiltViewModelto ViewModel - Use
@Inject constructorfor dependencies - Inject directly:
hiltViewModel()in Compose or ViewModelProvider - No factory needed—Hilt generates it
Hilt automatically creates the factory and manages ViewModel lifecycle. This eliminates ~20-30 lines of boilerplate per ViewModel and reduces bugs from manual factory management.
Scopes and Components: Managing Object Lifecycles
@Singleton lives with App. @ActivityScoped lives with Activity. Hilt manages destroying instances when the scope dies. Understanding scopes is crucial for proper lifecycle management:
Available Scopes:
- @Singleton: One instance for entire app lifetime
- @ActivityRetainedScoped: Survives Activity recreation (for ViewModels)
- @ActivityScoped: One instance per Activity
- @FragmentScoped: One instance per Fragment
- @ViewScoped: One instance per View
- @ViewWithFragmentScoped: Shared between Fragment and View
- @ServiceScoped: One instance per Service
Scope Hierarchy:
- Singleton outlives all other scopes
- ActivityScoped outlives FragmentScoped and ViewScoped
- Instances are destroyed when their scope is destroyed
Choose the narrowest scope that makes sense. Use @Singleton for truly app-wide data (database, API client). Use ActivityScoped for Activity-specific data. Hilt automatically cleans up instances when scopes are destroyed, preventing memory leaks.
Testing with Hilt: Swapping Dependencies
Use @HiltAndroidTest. You can easily swap modules to inject mock network clients during UI tests. Testing is where DI shines:
Unit Testing:
- Don't need Hilt—just create instances manually
- Inject mocks directly into classes
- Fast and simple for testing business logic
UI Testing with Hilt:
- Use
@HiltAndroidTeston test class - Create
@TestModuleto replace production modules - Use
@UninstallModulesto remove production modules - Hilt uses test modules instead of production ones
Testing Strategies:
- Mock Network: Replace Retrofit with mock API responses
- Fake Database: Use in-memory database for tests
- Test Repositories: Inject test implementations
- Isolated Tests: Each test gets fresh instances
Hilt's testing support is excellent. You can swap any dependency for testing without modifying production code. This enables true integration tests where you test your app with real UI but mocked backend.
Remember: the power of DI is most evident in testing. Without DI, testing would require extensive mocking frameworks or real infrastructure. With Hilt, testing becomes straightforward and reliable.