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
You are currently viewing page 2 of 4 of this article. Click here to view the first page.

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.

Note: The starter project already includes the dependencies necessary to use Dagger in all modules. Check out the respective 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:

  1. This entity is an object.
  2. 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:

  1. 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.
  2. 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 a NumberFactDataSource instance as its input argument. It’s your duty to tell Dagger how to construct this named data source.
  3. 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.
  4. 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.

Note: When declaring an interface dependency, Dagger recommends using the @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:

  1. a view, and
  2. 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.

Note: If you want to know more about scopes in Dagger, check out the official documentation about this topic.

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:

  1. 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.
  2. Bear in mind that the functions providing the views use these input arguments.
  3. 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.