Dependency Injection (DI) in Android
In Android development, Dependency Injection (DI) is a powerful design pattern that helps you create cleaner, modular, and easily testable applications. It allows you to manage how objects get the instances of other classes they depend on, without having to create them manually.
1. What Is Dependency Injection?
Dependency Injection means providing required dependencies to a class from the outside, instead of letting the class create them on its own. This makes your code loosely coupled and more maintainable.
// Without Dependency Injection
class UserRepository {
private val apiService = ApiService() // tightly coupled
}
// With Dependency Injection
class UserRepository(private val apiService: ApiService)
In the first example, UserRepository directly creates ApiService,
making it hard to test and reuse.
In the second example, ApiService is provided externally —
making it easier to manage and test.
2. Why Dependency Injection Exists
As Android apps grow, they contain many interconnected components like ViewModels, Repositories, and Services. Without DI, creating and maintaining these objects manually becomes complicated and repetitive. DI simplifies this by centralizing dependency management.
- Reduces code duplication.
- Improves testability (easily mock dependencies).
- Makes code modular and scalable.
- Decouples object creation from business logic.
3. Types of Dependency Injection
Dependency Injection can be implemented in several ways depending on how dependencies are provided to a class. Below are the three most common types:
- Constructor Injection – Dependencies are passed via constructor parameters.
- Field Injection – Dependencies are injected directly into fields or properties.
- Method Injection – Dependencies are passed through methods.
1. Constructor Injection
Dependencies are passed through the class constructor. This is the most recommended approach as it ensures immutability and makes testing easier.
// Example: Constructor Injection
class AuthRepository {
fun loginUser() = "User logged in successfully!"
}
class AuthViewModel(private val repository: AuthRepository) {
fun authenticate() = repository.loginUser()
}
// Usage
val viewModel = AuthViewModel(AuthRepository())
println(viewModel.authenticate())
2. Setter Injection (or Property Injection/Field Injection):
In Field Injection, dependencies are directly injected into fields or properties
of a class. This is especially useful in Android components like Activity or
Fragment, where you cannot pass dependencies through the constructor because
the Android system itself creates these components.
Frameworks like Dagger Hilt handle this automatically — you just need to
annotate your component with @AndroidEntryPoint and mark the dependency with
@Inject.
// Example: Field Injection using Hilt
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
@Inject
lateinit var repository: AuthRepository
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// Hilt has already injected the repository here
repository.loginUser()
}
}
How It Works:
@AndroidEntryPointtells Hilt that this component needs dependency injection.@Injectmarks which field should be provided automatically.- When the Activity is created, Hilt injects the dependency before
onCreate()runs.
Advantages:
- Ideal for Android system-managed classes like Activities and Fragments.
- Automatic and clean — no need to manually create dependencies.
- Reduces boilerplate code when combined with Hilt.
Disadvantages:
- Not ideal for unit testing — dependencies aren’t visible in the constructor.
- Hides dependency requirements, making code less explicit.
- Tightly coupled with the DI framework’s lifecycle.
⚠️ Tip: Use Field Injection only for Android framework classes. For ViewModels and business logic, prefer Constructor Injection.
3. Method Injection
Dependencies are passed through a method rather than a constructor or property. Useful when a dependency cannot be provided at construction time.
// Example: Method Injection
class AuthViewModel {
private lateinit var repository: AuthRepository
fun injectRepository(repo: AuthRepository) {
repository = repo
}
fun authenticate() = repository.loginUser()
}
// Usage
val viewModel = AuthViewModel()
viewModel.injectRepository(AuthRepository())
println(viewModel.authenticate())
4. Benefits of Using DI in Android
- Testability: Mock dependencies in unit tests easily.
- Reusability: Classes are no longer tied to specific implementations.
- Scalability: Manage complex object graphs efficiently.
- Maintainability: Reduces boilerplate and initialization code.
5. Manual DI vs Framework-based DI
You can manually provide dependencies or use frameworks like Dagger or Hilt to automate the process.
// Manual DI Example
val apiService = ApiService()
val repository = UserRepository(apiService)
val viewModel = UserViewModel(repository)
While manual DI works for small apps, frameworks like Hilt handle large dependency graphs automatically, reducing complexity.
6. Introducing Hilt (by Google)
Hilt is a DI library built on top of Dagger, designed specifically for Android. It integrates seamlessly with Android components and generates all boilerplate code.
Step 1: Add Dependencies
implementation "com.google.dagger:hilt-android:2.52"
kapt "com.google.dagger:hilt-compiler:2.52"
Step 2: Annotate Application Class
@HiltAndroidApp
class MyApp : Application()
Step 3: Create a Module to Provide Dependencies
@Module
@InstallIn(SingletonComponent::class)
object NetworkModule {
@Provides
fun provideApiService(): ApiService {
return ApiService()
}
}
Step 4: Inject Dependencies into Components
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
@Inject lateinit var apiService: ApiService
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
apiService.fetchData()
}
}
7. Scopes in Hilt
@Singleton– Single instance shared across the whole app.@ActivityScoped– Instance tied to the lifecycle of an Activity.@ViewModelScoped– Used with ViewModels.
8. Common Mistakes in DI
- Forgetting to annotate classes with
@AndroidEntryPoint. - Creating dependencies manually instead of injecting them.
- Injecting large objects unnecessarily (causing memory overhead).
- Mixing different scopes incorrectly (e.g., Singleton with ActivityScoped).
9. Best Practices
- Use constructor injection wherever possible.
- Keep your modules organized by feature (NetworkModule, RepositoryModule, etc.).
- Avoid overusing
@Singleton— scope wisely. - Use interfaces for flexible implementations (e.g., for testing).