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?
Building Up the Dagger Graph
As you may know, Dagger mainly works with components and modules. The latter are only necessary when dependency class constructors aren’t accessible and/or when an interface encapsulates the dependency class. This is the situation you’ll have to deal with, since, for example, the repository injected into FetchNumberFactUc
conforms to DomainlayerContract.Data.DataRepository
. You’ll see that this same thing happens to all related entities in the application.
Now you’ll analyze each module to build up the dependency graph.
build.gradle
files to see how.Connecting the Data Layer
Look at the data-layer module of the project, particularly at the repository folder. Open up NumberDataRepository
and you’ll see the following:
// 1
object NumberDataRepository : DomainlayerContract.Data.DataRepository<NumberFactResponse> {
// 2
private val numberFactDataSource: NumberFactDataSource by lazy { NumbersApiDataSource() }
...
Consider that:
- This entity is an
object
. - It requires a
NumberFactDataSource
— originally instantiated internally, which is definitely not what you want.
Create a folder called “di” and a file called DatalayerModule.kt
. In this file, you’ll add all the dependencies you want to make available from data-layer.
Start filling the file with the module definition of the repository you’ll inject:
@Module
object RepositoryModule {
@Provides
@Named(DATA_REPOSITORY_TAG) // 1
// 2
fun provideDataRepository(
@Named(NUMBER_FACT_DATA_SOURCE_TAG)
numberFactDs: NumberFactDataSource
): @JvmSuppressWildcards // 3
DomainlayerContract.Data.DataRepository<NumberFactResponse> =
// 4
NumberDataRepository.apply { numberFactDataSource = numberFactDs }
}
Follow Android Studio hints to add the necessary library imports (Alt-Enter
/Command-Enter
). Once done, pay special attention to the following:
- The
@Named
annotation allows discriminating between classes that conform to the same type. By default, in this sample application, all injections use this technique, although it’s not always necessary. - The function name declared isn’t important, since you won’t invoke it from any part of your code. Dagger will use it internally when a
DomainlayerContract.Data.DataRepository
instance is required. The function includes aNumberFactDataSource
instance as its input argument. It’s your duty to tell Dagger how to construct this named data source. - Dagger is written in Java and that’s why the compiler needs some extra annotations to translate from Kotlin. Due to type erasure,
@JvmSuppressWildcards
is needed when using generics. - Here’s where you define how the repository instance will be built. In this case, since it’s a singleton, any required variable initialization will take place in an
apply
block.
You’ll see an error when you try to assign numberFactDataSource
since the variable is private. To fix the issue, open NumberDataRepository
and replace the private val numberFactDataSource: NumberFactDataSource by lazy { NumbersApiDataSource() }
line with lateinit var numberFactDataSource: NumberFactDataSource
.
@Binds
annotation. In this tutorial, you won’t use this approach. The idea is to provide a common way to define modules.Populing the Data Layer
Now, using the same approach, add the rest of the data-layer dependencies. To do this, copy the following snippet at the end of the DatalayerModule
file you just created, below the RepositoryModule
object, taking care to include the necessary imports:
private const val TIMEOUT = 10L
@Module
class DatasourceModule {
@Provides
@Named(NUMBER_FACT_DATA_SOURCE_TAG)
fun provideNumberFactDataSource(ds: NumbersApiDataSource): NumberFactDataSource = ds // 1
@Provides
fun provideRetrofitInstance(): Retrofit = Retrofit.Builder()
.client(getHttpClient())
.addConverterFactory(ScalarsConverterFactory.create())
.addCallAdapterFactory(CoroutineCallAdapterFactory())
.baseUrl(NumberFactDataSource.BASE_URL)
.build()
}
fun getHttpClient(): OkHttpClient {
val interceptor = HttpLoggingInterceptor()
if (BuildConfig.DEBUG) {
interceptor.level = HttpLoggingInterceptor.Level.BODY
} else {
interceptor.level = HttpLoggingInterceptor.Level.NONE
}
return OkHttpClient.Builder()
.addInterceptor(interceptor)
.connectTimeout(TIMEOUT, TimeUnit.SECONDS)
.readTimeout(TIMEOUT, TimeUnit.SECONDS)
.build()
}
You may have seen that certain definitions aren’t yet present. More specifically, provideNumberFactDataSource
expects a NumbersApiDataSource
as its input argument. You’ll tell Dagger how to build this class instance adding an @Inject
annotation to its constructor.
Open NumberFactDataSource.kt
and modify NumbersApiDataSource
so it looks like the following:
class NumbersApiDataSource @Inject constructor(private val retrofit: Retrofit) :
NumberFactDataSource {
override suspend fun fetchNumberFact(request: NumberFactRequest): Response<String> =
retrofit.create(NumbersApiService::class.java)
.getNumberFactAsync(
number = request.number.toString(),
category = request.category.toString().toLowerCase(Locale.ROOT)))
}
Now the constructor includes retrofit
as a dependency. Go ahead and delete the getRetrofitInstance()
and provideHttpClient()
methods in the file, since you already added them when editing DatasourceModule
.
Domain Layer
It’s time to rearrange domian-layer. Navigate to that module and create a folder called “di” and a file called DomainlayerModule.kt
. Add all available dependencies from domain-layer.
Copy and paste the following snippet into the file:
@Module
object UsecaseModule {
@Provides
@Named(FETCH_NUMBER_FACT_UC_TAG)
fun provideFetchNumberFactUc(usecase: FetchNumberFactUc): @JvmSuppressWildcards DomainlayerContract.Presentation.UseCase<NumberFactRequest, NumberFactResponse> =
usecase
}
There’s only one dependency — a use case of type DomainlayerContract.Presentation.UseCase
— available for MainPresenter
. Later you’ll tell Dagger how to build FetchNumberFactUc
.
Open MainPresenter.kt
and substitute its constructor with:
class MainPresenter @Inject constructor(
@Named(MAIN_VIEW_TAG) private val view: MainContract.View, // 1
@Named(FETCH_NUMBER_FACT_UC_TAG) private val fetchNumberFactUc: @JvmSuppressWildcards DomainlayerContract.Presentation.UseCase<NumberFactRequest, NumberFactResponse> // 2
) : MainContract.Presenter {
...
In the code above, usecase
is injected through the class constructor instead of being initialized internally. In fact, you can see Dagger will inject two definitions:
- a view, and
- a use case
Remove the line private val fetchNumberFactUc ...
from the MainPresenter
since you’re now supplying that dependency via constructor arguments.
You also need to remove the line in onDetach()
that sets view to null.
Finally, update the constructor for FetchNumberFactUc
so dagger can instantiate it. Replace the constructor with the following:
class FetchNumberFactUc @Inject constructor(
You’ve added the @Inject
annotation, which will allow Dagger to automatically construct instances of the class.
You haven’t yet told Dagger how to provide the view
, but that will come later.
Before you move on to the presentation-layer, you need to quickly tweak the constructor for the FetchNumberFactUc
class. Open FetchNumberFactUc
and replace the class header with the following:
class FetchNumberFactUc @Inject constructor(
@Named(DATA_REPOSITORY_TAG)
private val numberDataRepository: @JvmSuppressWildcards DomainlayerContract.Data.DataRepository<NumberFactResponse>
) : DomainlayerContract.Presentation.UseCase<NumberFactRequest, NumberFactResponse> {
Adding the @Inject
annotation to the constructor and the @Named
annotation to the data repository will give Dagger all of the information it needs to construct an instance of FetchNumberFactUc
.
Presentation Layer
Last but not least, the dependencies available in the presentation-layer module are views — in Android, Activity
entities — and presenters.
However, stop one moment to declare a few custom scopes. Navigate to presentation-layer and create a folder called “di” and a file called Scopes.kt
. Fill it with the following content:
import javax.inject.Scope
@Scope
@Retention(AnnotationRetention.RUNTIME)
annotation class ApplicationScope
@Scope
@Retention(AnnotationRetention.RUNTIME)
annotation class ActivityScope
You’ll be using these scopes throughout the implementation so that Dagger doesn’t leak any instance.
Once done, create another file inside “di” called PresentationlayerModule.kt
. Since there are two features — “Splash” and “Main” — you’ll declare two different modules:
@Module
class SplashModule(private val activity: SplashActivity) { // 1
@ActivityScope
@Provides
@Named(SPLASH_VIEW_TAG)
fun provideSplashView(): SplashContract.View = activity // 2
@ActivityScope
@Provides
@Named(SPLASH_PRESENTER_TAG)
fun provideSplashPresenter(presenter: SplashPresenter): SplashContract.Presenter = presenter // 3
}
@Module
class MainModule(private val activity: MainActivity) { // 1
@ActivityScope
@Provides
@Named(MAIN_VIEW_TAG)
fun provideMainView(): MainContract.View = activity // 2
@ActivityScope
@Provides
@Named(MAIN_PRESENTER_TAG)
fun provideMainPresenter(presenter: MainPresenter): MainContract.Presenter = presenter // 3
}
Here’s a breakdown of the code above:
- There’s an important change in the above definition, since both modules have an argument in the constructor. Don’t worry about this for now. Later, when building the dependency graph, you’ll tell Dagger how to instantiate these modules.
- Bear in mind that the functions providing the views use these input arguments.
- To properly build the presenters, you’ll modify the constructors editing their definition files.
In fact, you already did this with MainPresenter
, so open SplashPresenter
and replace the constructor with:
class SplashPresenter @Inject constructor( @Named(SPLASH_VIEW_TAG) private val view: SplashContract.View? ) : SplashContract.Presenter { ...
Don’t forget to remove the line view = null
in onDetach()
, since this field is no longer mutable.