Chapters

Hide chapters

Dagger by Tutorials

First Edition · Android 11 · Kotlin 1.4 · AS 4.1

15. Dagger & Modularization
Written by Massimo Carli

Heads up... You're reading this book for free, with parts of this chapter shown beyond this point as scrambled text.

In the previous chapters, you learned about multibinding with Sets and Maps to improve the architecture of the Information Plugin Framework. With that information, you can add new features to the Busso App in an easy, pluggable and declarative way.

You’ve vastly improved Busso’s architecture, but there’s still room to make it even better. For instance, all the code is currently in the main app module. It would be nice to split that code into different modules to reduce the building time of your app while increasing its reusability and extensibility. But how can you do that with Dagger? Is it even possible?

The answer, of course, is yes! In this chapter, you’ll refactor the Busso App by moving some of the code from the main app module to other modules and changing the Dagger configuration accordingly.

Note: In this chapter, you’ll read the name module many times, referring to either the Gradle Module or the Dagger Module. To make everything clear, this chapter will refer to Gradle Modules as modules and Dagger Modules as @Modules.

What is modularization?

Modularization is the process of splitting the code and resources of your app into separate, smaller modules — in this case, Gradle modules. You can think of a Gradle module as a way to encapsulate code and resources. This makes it simpler to create a single library that you either use locally or publish in a repository like Artifactory to share with other developers.

A module might depend on other modules that you declare in the dependencies block of its build.gradle.

Note: You’ll use Gradle modules in this chapter, but the same concepts are valid with other systems, like Apache Maven or Apache Ivy.

Whether there’s a big advantage to using different modules in your app depends on how big and complex that app is. In general, using different modules gives you the following benefits:

  • Better organization and encapsulation of the code.
  • Shorter building time.
  • The opportunity to create libraries that make the code more reusable.
  • Better ownership management.

This chapter will cover each point in more detail, giving you the chance to improve Busso along the way.

Start by opening the Busso App from the starter folder of the materials for this chapter in Android Studio.

You’ll get an initial source folder structure, as Figure 15.1 shows:

Figure 15.1 — Initial Busso App source folders structure
Figure 15.1 — Initial Busso App source folders structure

As you see, there are a few new modules, one of which is libs.di.scopes.

At the moment, the new modules are all empty. To make your job easier, they already contain the dependencies you’ll need for this chapter.

Figure 15.2 — The Scopes module source files
Figure 15.2 — The Scopes module source files

Speaking of reusability, this module already contains the definition of the different @Scopes you created in the previous chapters. As you see in Figure 15.2, you have:

  • ApplicationScope
  • ActivityScope
  • FragmentScope

This module is very simple. It only depends on the javax.inject library, as you see by opening its build.gradle:

plugins {
  id 'kotlin'
}
apply from: '../../../versions.gradle'

dependencies {
  api "javax.inject:javax.inject:$javax_annotation_version"
  implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
}

You’ll learn all about the other modules later.

Better organization and encapsulation of the code

As you know, encapsulation is one of the fundamental concepts in object-oriented programming, but it’s also a principle to use in higher-level contexts like a library or a full architecture.

Figure 15.3 — The Navigation module source files
Layate 63.9 — Vnu Xiholoceon noqano siubme zozup

// HERE
internal class NavigatorImpl(private val activity: Activity) : Navigator {
  override fun navigateTo(destination: Destination, params: Bundle?) {
  // ...
  }
}
Cannot access 'NavigatorImpl': it is internal in 'com.raywenderlich.android.ui.navigation'
@Module(includes = [ActivityModule.Bindings::class])
class ActivityModule {
  // ...
  @Provides
  @ActivityScope
  fun provideNavigator(activity: Activity): Navigator = NavigatorImpl(activity) // HERE
} 

Defining an external @Module

As you know, you use @Module to tell Dagger how to create the objects that are part of a dependency graph. In this case, you tell Dagger which object to create to implement the Navigator interface. You do this in ActivityModule.kt, which is in the main app.

@Module
object NavigationModule {

  @Provides
  @ActivityScope
  fun provideNavigator(activity: Activity): Navigator = NavigatorImpl(activity)
} 
@Module(
  includes = [
    NavigationModule::class // 1
  ]
)
interface ActivityModule { // 2

  @Binds
  fun bindSplashPresenter(impl: SplashPresenterImpl): SplashPresenter

  @Binds
  fun bindSplashViewBinder(impl: SplashViewBinderImpl): SplashViewBinder

  @Binds
  fun bindMainPresenter(impl: MainPresenterImpl): MainPresenter
}

Reducing build time

In large, professional apps, you usually have thousands of source and resource files. As you learned above, a Gradle module is basically a compilation unit. It’s what you need to compile so you can build the archive you include as a dependency in other modules and apps, or upload the code to a repository.

Creating libraries

Right now, the libs.ui.navigation module is in the same Busso project. However, you could also publish it to an external repository and just leave a dependency to it in the build.gradle of the main app. In that case, it would be like any other external library you use, including Retrofit or Dagger itself. Having all classes and resources in the same app module increases build times and makes code harder to manage.

Ownership management

Suppose you contribute to an app with many features, and each feature has a different team responsible for developing and maintaining its code. Then, imagine if all the code was in the main app module.

Busso App modularization

Now that you know the main reasons to modularize, and you’ve seen the example of the libs.ui.navigation module, it’s time to continue refactoring the Busso App.

The networking module

In the starter project, the network package in the app module contains all the code you need for Busso’s networking layer. Look at this code and you’ll see that some of the definitions are:

@Module
class NetworkModule {
  @Provides
  @ApplicationScope
  fun provideCache(application: Application): Cache = // 1
    Cache(application.cacheDir, 100 * 1024L)// 100K

  @Provides
  @ApplicationScope
  fun provideHttpClient(cache: Cache): OkHttpClient = // 2
   // ...

  @Provides
  @ApplicationScope
  fun provideRetrofit(httpClient: OkHttpClient): Retrofit = // 3
  	// ...

  @Provides
  @ApplicationScope
  fun provideBussoEndPoint(retrofit: Retrofit): BussoEndpoint { // 4 
    return retrofit.create(BussoEndpoint::class.java)
  }
}

Defining the networking configuration abstraction

The components you want to put in the libs.networking module might need some configuration values. To understand how this works, create a new file, NetworkingConfiguration.kt in the libs.networking module and add the following code:

interface NetworkingConfiguration {
  val cacheSize: Long // 1
  val serverBaseUrl: String // 2
  val dateFormat: String // 3
}

Creating the NetworkingModule

Now, you need to create a @Module to tell Dagger how to create the object you need to connect Busso to the server using the configuration data the main app module provides.

@Module
object NetworkingModule {

  @Provides
  @ApplicationScope
  fun provideCache(
    networkingConfiguration: NetworkingConfiguration, // 1
    application: Application
  ): Cache =
    Cache(
      application.cacheDir,
      networkingConfiguration.cacheSize // 1
    )

  @Provides
  @ApplicationScope
  fun provideHttpClient(cache: Cache): OkHttpClient =
    OkHttpClient.Builder()
      .cache(cache)
      .build()

  @Provides
  @ApplicationScope
  fun provideRetrofit(
    networkingConfiguration: NetworkingConfiguration, // 2
    httpClient: OkHttpClient
  ): Retrofit =
    Retrofit.Builder()
      .baseUrl(networkingConfiguration.serverBaseUrl) // 2
      .addCallAdapterFactory(RxJava2CallAdapterFactory.create())
      .addConverterFactory(
        GsonConverterFactory.create(
          GsonBuilder()
            .setDateFormat(networkingConfiguration.dateFormat) // 2
            .create()
        )
      )
      .client(httpClient)
      .build()
}

Adding the dependency to the networking module

This step is very simple. Open build.gradle for the main app module and apply the following changes:

// ...
dependencies {
  // ...
  // Network Apis  TO_BE_REMOVED // 1
  // implementation "com.squareup.retrofit2:retrofit:$retrofit_version"
  // implementation "com.squareup.retrofit2:converter-gson:$retrofit_gson_converter_version"
  // implementation "com.squareup.retrofit2:adapter-rxjava2:$retrofit_rx2_adapter_version"

  // Networking library
  implementation project(path: ':libs:networking') // 2
  // ...
}

Adding NetworkingConfiguration to the app

libs.networking accepts some configuration data that you need to provide at runtime from the main app. Open Configuration.kt in the conf package and change it like this:

const val BUSSO_SERVER_BASE_URL = "https://busso-server.herokuapp.com/api/v1/"

object BussoConfiguration : NetworkingConfiguration { // 1
  override val cacheSize: Long
    get() = 100 * 1024L // 100K // 2
  override val serverBaseUrl: String
    get() = BUSSO_SERVER_BASE_URL // 3
  override val dateFormat: String
    get() = "yyyy-MM-dd'T'HH:mm:ssZ" // 4
}

Adding NetworkingConfiguration to @ApplicationComponent’s dependency graph

Now, you need to tell Dagger that you want to use BussoConfiguration as the NetworkingConfiguration implementation to add to the dependency graph for @ApplicationScope. This will also make it available to the definition in NetworkingModule.

@Component(
  dependencies = [NetworkingConfiguration::class], // 1
  modules = [
    ApplicationModule::class,
    InformationPluginModule.ApplicationBindings::class,
    InformationSpecsModule::class 
  ]
)
@ApplicationScope
interface ApplicationComponent {
  // ...
  @Component.Factory
  interface Factory {

    fun create(
      @BindsInstance application: Application,
      networkingConfiguration: NetworkingConfiguration // 2
    ): ApplicationComponent
  }
}
class Main : Application() {
  // ...
  override fun onCreate() {
    super.onCreate()
    appComponent = DaggerApplicationComponent
      .factory()
      .create(this, BussoConfiguration) // HERE
  }
}

Updating NetworkModule.kt

Open NetworkModule.kt in the network package of the app module and change it to this:

@Module(
  includes = [
    NetworkingModule::class // HERE
  ]
)
object NetworkModule {

  @Provides
  @ApplicationScope
  fun provideBussoEndPoint(retrofit: Retrofit): BussoEndpoint {
    return retrofit.create(BussoEndpoint::class.java)
  }
}

What you achieved

With the help of the UML diagram in Figure 15.4, you can not get an overview of what you’ve achieved so far.

Figure 15.4 — The networking module UML diagram
Sucabe 81.1 — Hlu gavdabvilx duvoke UST tiukvuc

Figure 15.5 — Circular dependencies
Qavaxe 44.2 — Laxpovuv dikaxtefyeug

The location module

To modularize the location module, you need to make a few changes similar to the ones you already made for the networking module. In this case, you need to:

Figure 15.6 — The Location Rx Module
Yavova 61.4 — Wca Fudeqiep Sh Jerume

The plugins module

Refactoring the Information Plugin Framework is the most complex example of modularization in the Busso App.

Figure 15.7 — Information Plugin Framework dependencies
Yovupe 46.6 — Uqbocvatoaq Qmimav Qdekuxakn wuceffadceum

Figure 15.8 — The Busso App
Cozezi 21.3 — Jla Yagna Ids

Key points

  • Modularization is a fundamental step in the development process of a project.
  • Splitting your code into external modules can improve the build time of your app and make your code more reusable.
  • Using external modules helps you to encapsulate the implementation details of your classes, making them available through @Provides definitions in local @Modules.
  • You set a simple Kotlin interface as the dependency for a @Component and use it to pass information from the main app to a dependent module.
Have a technical question? Want to report a bug? You can ask questions and report bugs to the book authors in our official book forum here.
© 2024 Kodeco Inc.

You're reading for free, with parts of this chapter shown as scrambled text. Unlock this book, and our entire catalogue of books and videos, with a Kodeco Personal Plan.

Unlock now