1. Introduction

Dagger 2 is one of the most popular dependency injection frameworks for Android and Java applications. By automating dependency management, Dagger simplifies object creation and wiring in complex applications. Constructor injection is a powerful feature in Dagger 2, where dependencies are provided through the constructor of a class.

In this tutorial, we’ll use Dagger 2 to inject dependencies into a class’s constructor in Kotlin. Specifically, we’ll see how to work with named arguments when a class requires multiple instances of the same type. Named arguments help differentiate between various instances of the same dependency type, improving code clarity and maintainability.

2. Overview of Dagger 2 Constructor Injection

In Dagger 2, constructor injection allows dependencies to be injected directly into a class’s constructor. This allows objects to be instantiated with all the necessary dependencies. Dagger can automatically provide the required dependencies as long as it knows how to create them:

class NetworkClient @Inject constructor(private val apiService: ApiService)

In this example, Dagger 2 injects an instance of ApiService into NetworkClient when it is created.

3. Using @Named for Multiple Instances and Conditional Injection

When working with Dagger 2, there are cases where we need to inject multiple dependencies of the same type, each with different configurations. For example, an ApiService can have a debug and release implementation. To manage this, we use the @Named annotation to differentiate between these instances.

Let’s start by defining the ApiService interface and its two implementations:

interface ApiService {
    fun getData(): String
}

class DebugApiService : ApiService {
    override fun getData(): String {
        return "Debug Data"
    }
}

class ReleaseApiService : ApiService {
    override fun getData(): String {
        return "Release Data"
    }
}

To configure Dagger to inject the appropriate instance, we use @Named in the module where the services are provided:

@Module
class AppModule {

    @Provides
    @Named("debug")
    fun provideDebugApiService(): ApiService = DebugApiService()

    @Provides
    @Named("release")
    fun provideReleaseApiService(): ApiService = ReleaseApiService()
}

With this setup, Dagger can distinguish between the two instances of ApiService when injecting into a class. Our module provides multiple ApiService instances with different names as declared with @Named.

3.1. Conditional Injection

If we only need one version of a dependency based on the application’s environment, then we can use conditional injection at bean creation. We can configure Dagger to choose the appropriate instance at runtime. Let’s modify the module to select the ApiService instance based on an environment variable:

@Module
class AppModule {

    @Provides
    @Named("apiService")
    fun provideApiService(): ApiService {
        val environment = System.getenv("APP_ENV") ?: "production"
        return if (environment == "development") {
            DebugApiService()
        } else {
            ReleaseApiService()
        }
    }
}

This approach ensures that the correct version of ApiService is injected depending on the environment configuration, making it easy to manage different environments without changing the core application logic.

3.2. Constructor Injection with @Named

Finally, we can inject multiple instances of ApiService directly into a class using @Named annotations:

class NetworkClient @Inject constructor(
    @Named("debug") private val debugApiService: ApiService,
    @Named("release") private val releaseApiService: ApiService
)

In this example, Dagger injects the appropriate ApiService instances into NetworkClient, based on the @Named qualifiers. This provides flexibility in managing different configurations and ensures both dependencies are injected in the correct place of our client.

4. Unit Testing with @Named Dependencies and Mocking

In unit tests, especially when using mocking libraries like Mockk or Mockito, we often want to provide mock implementations of dependencies while leveraging Dagger for injection. Using @Named annotations helps us mock each dependency differently when we have multiple instances of the same type.

4.1. Setting Up Test Modules with Mocked @Named Dependencies

First, we need to create a Dagger module for the test where we provide the mocked instances of the @Named dependencies. Let’s assume we’re mocking both the DebugApiService and ReleaseApiService:

@Module
class TestAppModule {

    @Provides
    @Named("debug")
    fun provideMockDebugApiService(): ApiService = mockk {
        every { getData() } returns "Mocked Debug Data"
    }

    @Provides
    @Named("release")
    fun provideMockReleaseApiService(): ApiService = mockk {
        every { getData() } returns "Mocked Release Data"
    }
}

In this example, we’re using Mockk to create mock implementations of both versions of the ApiService.

4.2. Defining the Test Component

Next, we also need a Dagger component that includes the test module and injects the necessary mocks into the class under test:

@Component(modules = [TestAppModule::class])
interface TestAppComponent {
    fun networkClient(): NetworkClient
}

Specifically, this component is responsible for providing the mocked dependencies and fully constructing the NetworkClient, which will have all the @Named dependencies injected.

4.3. Injecting Mocks into the Test Class

In our JUnit test, instead of manually creating an instance of NetworkClient, we can now use the component to retrieve a fully-instantiated NetworkClient instance:

class NetworkClientTest {

    private lateinit var networkClient: NetworkClient

    @Before
    fun setUp() {
        val testComponent = DaggerTestAppComponent.create()
        networkClient = testComponent.networkClient()
    }

    @Test
    fun `test NetworkClient with mock dependencies`() {
        assertEquals("Mocked Debug Data", networkClient.getDebugData())
        assertEquals("Mocked Release Data", networkClient.getReleaseData())
    }
}

In the setUp() method, we create our test Dagger component and retrieve the fully constructed NetworkClient instance. This approach ensures that NetworkClient has all the necessary dependencies injected, including the mocked ApiService instances annotated with @Named. The test then verifies the mock behavior to show that different mocks were injected into the different instances of ApiService in NetworkClient.

5. Conclusion

In this article, we explored how to use Dagger 2 to inject dependencies via constructors in Kotlin, specifically focusing on how to handle multiple instances of the same type using @Named annotations. This approach allows for greater flexibility when managing multiple dependencies of the same type.

By leveraging named beans, we can inject different implementations of the same interface or class, thus making our applications more modular and easier to test.


原始标题:Dagger 2 Constructor Injection with Named Arguments in Kotlin