Dagger in Multi-Module Clean Applications

In this tutorial, you’ll learn how to integrate Dagger in an Android multi-module project built using the clean architecture paradigm. By Pablo L. Sordo Martinez.

Leave a rating/review
Download materials
Save for later
Share

There are several things to consider when designing and developing an Android app. These include architecture, class hierarchy, package structure and the tech stack.

This tutorial will focus on:

  • Multi-module apps
  • Clean architecture
  • Dependency injection with Dagger

In particular, it’ll show how the above implementations work together.

You’ll also use several other tools and techniques, including:

  • Extensive application of the abstraction principle. Every entity in the project conforms to an interface. This ensures flexibility and maintainability.
  • Coroutines. Since the application is written in Kotlin, it’s based on coroutines. You can look at DomainLayerContract, particularly at the UseCase interface.
  • The inclusion of certain functional programming features, thanks to the Arrow library. You can see this on the service queries’ responses, which are typed with Either.
Note: This tutorial assumes you’re comfortable working in Kotlin and using Dagger. If not, first check out Kotlin For Android: An Introduction and Dependency Injection in Android with Dagger 2 and Kotlin.

Getting Started

Download the starter project by clicking the Download Materials button at the top or bottom of the tutorial.

To learn about the concepts above, you’ll create an app named Numberizer, which allows you to fetch information about numbers using a public API. During the process, you’ll see how to implement a dependency injection scheme into a multi-module app, with clean architecture, from scratch.

sample project structure

Open the project in Android Studio and take a quick look at it. The app structure is consistent with the clean paradigm, which you’ll learn about soon. Once the app is running in your favorite device or emulator, type any number in the provided text box and hit the button.

Initial numberizer page

After a second, a toast message will display saying “Unknown error”.

To figure out what’s going on here, follow the code starting from MainActivity, where the button listener lambda is invoked (line 50). You’ll see the problem resides in FetchNumberFactUc:

class FetchNumberFactUc : DomainlayerContract.Presentation.UseCase<NumberFactRequest, NumberFactResponse> {
    // 1
    private lateinit var numberDataRepository: DomainlayerContract.Data.DataRepository<NumberFactResponse>

    override suspend fun run(params: NumberFactRequest?): Either<Failure, NumberFactResponse> =
        params?.let {
            // 2
            if (::numberDataRepository.isInitialized) {
                numberDataRepository.fetchNumberFact(request = params)
            // 3
            } else {
                Failure.Unknown().left()
            }
        } ?: run {
            Failure.InputParamsError().left()
        }

}

In the code above:

  1. numberDataRepository relates to a repository instance that needs to be initiated at some point.
  2. Due to this early stage of the implementation, the above variable is checked before use. You’ll change this and other similar features during this tutorial.
  3. Since there hasn’t been any initialization of the repository variable, the function returns Failure.Unknown.

There are similar defects across the project. You’ll soon sort out these problems so that Numberizer is functional. But before diving head first into the implementation, you’ll cover some key concepts.

Covering Key Concepts

This section serves as a brief overview of the foundations of the aforementioned concepts.

Multi-Module App

Creating an app comprised of multiple modules is definitely not a novelty in Android. Modules have always been available in Android Studio. However, Google hasn’t advocated for them — not much, at least — until recently, when dynamic features came out.

Organizing your logic and utilities in distinct modules provides flexibility, scalability and code legibility.

Clean Architecture

There are several options when it comes to software architecture. In this tutorial, you’ll use a class hierarchy based on the clean architecture paradigm.

Among the existing implementations of clean architecture, there are remarkable contributions such as the ones from Antonio Leiva and Fernando Cejas.

The project you’ll start off with consists of several layers. Each of these entities is in charge of certain responsibilities, which are handled in isolation. All them are interconnected through interfaces, which allows you to achieve the necessary abstraction between them.

Here’s a bit about each layer:

  • Presentation: This layer’s duties consist of managing events caused by user interactions and rendering the information coming from the domain layer. You’ll be using the well-known Model-View-Presenter (MVP) architecture pattern. This entity “sees” the domain layer.
  • Domain: This layer is in charge of the application business logic. It’s built upon use cases and repositories — see the Repository Pattern for more information. This entity only contains Kotlin code, so testing consists of unit tests. This layer represents the most inner entity, and thus it doesn’t “see” any layer other but itself.
  • Data: This layer provides data to the application (data sources). You’ll be using Retrofit for service queries. This layer “sees” the domain layer.

Using clean architectures lets you make your code more SOLID. This makes applications more flexible and scalable when implementing new functionality, and it makes testing easier.

Dependency Injection With Dagger

The last pillar of the implementation you’ll be working with is dependency injection. Although Hilt is new and Koin is gaining supporters, you’ll be using vanilla Dagger. More specifically, you’ll use dependency injection with Dagger in an easy and straightforward way.

Note: While it may seem like overkill to use these techniques for such a simple app, using clean architecture and dagger can help you to build a scalable robust architecture as your application grows.

Now that you have a better overview, it’s time to dive deeper into the theory!

Analyzing the Problem

The implementation for FetchNumberFactUc shown ealier has one main problem, apart from the obvious unwanted return value: It depends on a repository declared internally through lateinit var. This makes the class and its functions difficult to test, since those dependencies can’t be mocked and stubbed in unit tests.

To confirm this, look at FetchNumberFactUcTest. This file shows two unit tests for this use case. If you run it, you’ll see the second test fails because the assertion doesn’t succeed:

@Test
fun `Given right parameters, when usecase is invoked -- 'NumberFactResponse' data is returned`() = runBlockingTest {
    // given
    val rightParams = NumberFactRequest(number = DEFAULT_INTEGER_VALUE)
    // when
    val response = usecase.run(params = rightParams)
    // then
    Assert.assertTrue(response.isRight() && (response as? Either.Right<NumberFactResponse>) != null)
}

This outcome is expected, since the repository can’t be mocked and stubbed, and thus the return value remains unchanged and equal to Failure.Unknown().

The key to fixing this use case is to provide it externally with any dependency it needs:

class FetchNumberFactUc(
    private val numberDataRepository: DomainlayerContract.Data.DataRepository<NumberFactResponse>
) : DomainlayerContract.Presentation.UseCase<NumberFactRequest, NumberFactResponse> {

    override suspend fun run(params: NumberFactRequest?): Either<Failure, NumberFactResponse> =
        params?.let {
            numberDataRepository.fetchNumberFact(request = params)
        } ?: run {
            Failure.InputParamsError().left()
        }

}

Now, instead of having a lateinit var numberDataRepository that could be uninitialized, FetchNumerFactUc takes one constructor parameter – the numberDataRepository. Now you have full access to the repository instance utilized by the use case. However, this solution will force you to tell the corresponding presenter — MainPresenter in this case — which repository to use when invoked by the related view — MainActivity in this case.

That means MainActivity will have to know how to build and provide the specific type of repository this usecase needs. What a mess! This is when a developer begins to appreciate a dependency injection mechanism such as Dagger. The key is to build a container of dependencies that will deliver any instance when required.

Note: The project also includes a test file for MainPresenter, which has a failing case too. Don’t worry; you’ll address this later.