Android Core

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:
  1. @AndroidEntryPoint tells Hilt that this component needs dependency injection.
  2. @Inject marks which field should be provided automatically.
  3. 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).