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.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
Dagger in Multi-Module Clean Applications
30 mins
- Getting Started
- Covering Key Concepts
- Multi-Module App
- Clean Architecture
- Dependency Injection With Dagger
- Analyzing the Problem
- Building Up the Dagger Graph
- Connecting the Data Layer
- Populing the Data Layer
- Domain Layer
- Presentation Layer
- Getting Familiar with Components and Subcomponents
- Buliding Activity Components
- Building the Application Component
- Wrapping Everything Up
- Performance Assessment
- Testing
- Testing the Presenter
- Testing the Use Case
- Testing the Repository
- Where to Go From Here?
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 theUseCase
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
.
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.
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.
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:
-
numberDataRepository
relates to a repository instance that needs to be initiated at some point. - 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.
- 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.
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.
MainPresenter
, which has a failing case too. Don’t worry; you’ll address this later.