15.
Dagger & Modularization
Written by Massimo Carli
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:
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.
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:
ApplicationScopeActivityScopeFragmentScope
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.
Encapsulating also means hiding the implementation details and exposing only the main abstractions that form what you call public APIs. This is part of what you learned in the very first chapter of this book: Encapsulation, or hiding details, is a way to reduce or avoid dependency.
ui.navigation is a simple example of the application of this principle. It’s also a good starting point to modularize Busso.
Look at the source files of libs.ui.navigation in Figure 15.3 and you’ll find a:
- Destination.kt file.
-
Navigatorinterface. -
NavigatorImplclass.
In Destination.kt, you just have the Destination sealed class with its implementations. These should stay public because they’re just data classes that encapsulate values, not behavior.
Navigator is an interface that’s public by definition. You should hide NavigatorImpl, instead.
Open NavigatorImpl.kt in libs.ui.navigation and make it internal, as in the following code:
// HERE
internal class NavigatorImpl(private val activity: Activity) : Navigator {
override fun navigateTo(destination: Destination, params: Bundle?) {
// ...
}
}
Build the app now and you’ll get the following error:
Cannot access 'NavigatorImpl': it is internal in 'com.raywenderlich.android.ui.navigation'
That’s because you’re trying to access NavigatorImpl from ActivityModule.kt in app.
@Module(includes = [ActivityModule.Bindings::class])
class ActivityModule {
// ...
@Provides
@ActivityScope
fun provideNavigator(activity: Activity): Navigator = NavigatorImpl(activity) // HERE
}
But NavigatorImpl is now internal and, therefore, only visible to libs.ui.navigation. Next, you’ll see how to fix this problem.
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.
Create a new di package in libs.ui.navigation and add a new NavigationModule.kt with the following code:
@Module
object NavigationModule {
@Provides
@ActivityScope
fun provideNavigator(activity: Activity): Navigator = NavigatorImpl(activity)
}
Here, you moved the @Provides definition for Navigator from ActivityModule.kt in the app module to NavigationModule.kt in libs.ui.navigation. Now, you can access NavigatorImpl from a place where it’s visible.
The NavigationModule definition needs the dependencies from the Dagger libraries. You also need to install the kapt plugin in build.gradle for the module. These dependencies have been already set up for you in the starter project.
Of course, you need to remove the same @Provides definition from ActivityModule.kt, where you add NavigationModule as value of the includes attribute. Because ActivityModule contains only @Binds definitions, you can make it an interface like this:
@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
}
Here, you just:
- Add
NavigationModuleas a value of theincludesattribute. - Convert the existing class into an interface containing all the existing
@Bindsdefinitions.
Now, you can successfully build and run the app.
In libs.ui.navigation, you see your first, simple example of how to hide implementation details in a module and how this works with @Modules. In this case, you:
- Reduced
NavigatorImpl’s visibility by using theinternalmodifier. As a result, you’ve hiddenNavigatorImplfrom the main app and removed the dependency on it. - Created a new
@Modulein ui.navigation, which includes@Providesfor theNavigatorimplementation. Of course, this means you have to fix the dependencies on the Dagger libraries andkaptplugin in build.gradle for the navigation module. - Added
NavigationModuleto the ones you include inActivityModule.
This means you can eventually use a different Navigator implementation without a single change in the main app.
Later in this chapter, you’ll see more complex use cases.
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.
If you have small modules, you also have small compilation units, which means shorter building times. For example, suppose you have a monolithic app that contains 100 classes you can usually build in 100 seconds. If you change a class, you need to rebuild 100 classes and their related resources, which will take about the same time: 100 secs.
Gradle is smart enough to avoid repeating some of the tasks in the building process. Nevertheless, some of the tasks, like compile and assemble, need to be done.
Suppose now that you have 100 classes, but split them into five modules with 20 classes each. If you change a class in one module, you only need to compile that module and its 20 classes.
The building time won’t be 1/5 of the original, because using modules introduces some overhead, but large apps will show noticeable improvement.
Now, in the previous libs.ui.navigation example, you won’t need to build the module anymore unless you change some of the files in it.
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.
Publishing a library is a good solution when you:
- Don’t change the code frequently.
- Want to make the library open source and allow other people to contribute.
- Need the same module in different apps.
A public library isn’t a good option if you:
- Need to change the code very frequently.
- You can’t or don’t want to share the code.
As with many decisions, it isn’t always black or white. For instance, if you publish your library to a private repository and need to change the code frequently, you can just depend on the SNAPSHOT of the library. Or you might use it as a local module at an early stage of your project, then open-source it after publishing it to an external repository.
In any case, you should consider this question during your development process.
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.
The developers from the different teams would all work on the same codebase. This is possible, but you could end up with severe problems because, in big companies, you have hundreds of developers working on the same app. For instance, you’ll likely have a lot of conflicts in your pull requests because there are no formal boundaries between the code of the different features.
One solution is to split the work on a package basis, but this only works well in small apps. For a big project, it wouldn’t make any difference.
Giving ownership of one or more modules to each team is a better way to manage the code and its lifecycle, avoiding conflicts and creating physical boundaries in the codebase.
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.
Some modules are already available in the starter project and you’ll add code to them, following along with the progression. You’ll work on different modules, in particular on:
- networking
- location
- plugins
In each case, you’ll learn what to do in situations with increasing complexity.
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:
- Generic, and can be reused in other projects.
- Specific to Busso.
As a rule, you should keep anything that depends on the specific application in the app and move what you can reuse to a different module.
In NetworkModule.kt, you’ll see this:
@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)
}
}
In this code, you provide implementations for:
CacheOkHttpClientRetrofitBussoEndpoint
Points 1, 2 and 3 relate to reusable objects and 4 is specific to Busso. You’d like to make the reusable code configurable from the main app. How can you do that?
In the starter project, you already have an empty libs.networking module available that includes build.gradle with all the dependencies you need.
Note: In the code for the Busso App, you’ll see some .keep files that are there to force Git to keep the empty folders in the repository. You can delete these files when the related folder is no longer empty.
What you need to do is:
- Find a way to abstract any configuration parameters the networking module requires.
- Create
NetworkingModulewith the bindings you can reuse. - Add the dependency to the networking module in the build.gradle of the main app module.
- Implement the configuration parameters for the abstraction.
- Change
ApplicationComponentso it can provide the configuration object. - Update NetworkModule.kt in the app module.
These are a bunch of steps, but they’re easy enough to code along with.
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
}
Here, you define an interface that requires you to implement properties referring to the:
- Size of the
Cache. - Base URL for the connection.
- Format to use for unmarshalling the dates.
The module doesn’t know this information yet; the main app module should provide it. You also need to use this information in the new @Module, which you’ll create in the next step.
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.
Create a new di package in libs.networking and add a new file, NetworkingModule.kt, with the following code:
@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()
}
In this code, you assume that an implementation of NetworkingConfiguration will eventually be available at runtime as part of the dependency graph for @ApplicationScope. Because of this, you:
- Add
NetworkingConfigurationas a parameter forprovideCache()and use itscacheSizeproperty in theCacheconstructor. - Do the same for
provideRetrofit(), using theserverBaseUrlanddateFormatproperties to initialize theRetrofitimplementation instance.
So far in this module, you haven’t provided the implementation of BussoEndpoint that’s specific to Busso. The next step is to make this module available to the main app.
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
// ...
}
Here, you:
- Remove the existing dependencies on the libraries for
Retrofit. - Add the dependency to the networking module, which will also provide the dependencies you removed previously as transitive dependencies.
You’ve now completed the networking module — it’s time to work on the app.
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
}
In this code, you:
-
Create
BussoConfigurationas the implementation of theNetworkingConfigurationinterface. -
Provide a value for
cacheSize. -
Do the same for
serverBaseUrlusing the existingBUSSO_SERVER_BASE_URLconstant, which you’ll also need in other places. -
Provide the
dateFormat.
But how do you tell Dagger to use this object as the implementation of NetworkingConfiguration to add to the dependency graph for the @ApplicationScope? You’ll do that in the next step.
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.
It’s important to note that NetworkingConfiguration is a normal Kotlin interface and not a Dagger @Component. The good news is that this doesn’t stop you from using any implementation — BussoConfiguration, in this case — as an actual @Component that you can create a dependency on.
Open ApplicationComponent.kt in the di package of the app module and apply the following changes:
@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
}
}
In this code, you:
- Add
NetworkingConfiguration::classas a value for the dependencies attribute of the@Componentannotation. This tells Dagger that yourApplicationComponentdepends on the object you’ll provide through an implementation ofNetworkingConfiguration. - Tell Dagger that you’re going to provide the
NetworkingConfigurationimplementation as a parameter of theFactoryDagger will generate for you.
Changing Factory means you need to make a change when you create the instance of ApplicationComponent. To do this, open Main.kt in the main package of the app module and apply the following change:
class Main : Application() {
// ...
override fun onCreate() {
super.onCreate()
appComponent = DaggerApplicationComponent
.factory()
.create(this, BussoConfiguration) // HERE
}
}
With this code, you pass BussoConfiguration as a second parameter of create().
You’re almost there! Now, you just need to remove the code you no longer need from NetworkModule.kt.
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)
}
}
Here, you only add the NetworkingModule in includes and remove the @Provides definitions that are now in the networking module. This @Module is now an object and not a simple class.
Now, you can finally build and successfully run the app!
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.
Following the modularization process for the Busso App, you wanted to move the reusable code for networking into a different networking module. In particular, you wanted to move the code that defines Cache, HttpClient and Retrofit. You created NetworkingModule to hold this code.
The problem was that these objects require information that’s specific to a particular app, like the base URL of the connection or the format to use when parsing dates.
The networking module cannot depend on the app module. The latter already depends on the former, so it would give you a circular dependency, as shown in Figure 15.5.
NetworkingConfiguration is the solution. NetworkingModule accesses the configuration data it needs through an implementation of NetworkingConfiguration that the main app provides. But how?
The magic happens because the app and the networking modules both contribute to the dependency graph for @ApplicationScope, which you define with ApplicationComponent.
So, using the dependencies attribute of @Component, you added a NetworkingConfiguration implementation to the dependency graph for ApplicationComponent. Using a new parameter in the @Component.Factory for ApplicationComponent, you added BussoConfiguration as the NetworkingConfiguration implementation to use.
Now, all the objects that both the app and the networking modules need are in the dependency graph for @ApplicationScope.
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:
- Move the
LocationModuledefinition into the di package in the libs.location.rx module. - Move
GeoLocationPermissionCheckerImplinto the permission package in the libs.location.rx module. - Encapsulate
GeoLocationPermissionCheckerImplas aGeoLocationPermissionCheckerimplementation, making itinternal. - Fix the imports in the app module due to some changes in the destination package. This should happen in ApplicationModule.kt in the app module.
After you’ve done these steps, the libs.location.rx source structure should look like the one in Figure 15.6:
Now, LocationModule is the only thing you need if you want to use Observable<LocationEvent> in the main app module.
The plugins module
Refactoring the Information Plugin Framework is the most complex example of modularization in the Busso App.
Note: A step-by-step description of the modularization process for the Information Plugin Framework would require too much space. You could do this yourself as an interesting and challenging exercise, or simply open the Busso App project in the final folder of the material for this chapter and look at the new structure.
To understand the new structure, look at the dependency diagram in Figure 15.7:
In this diagram, you see that:
- The main app module depends on the weather and whereami modules. You install the different plugins by adding their module in InformationSpecsModule.kt in the plugins package of the app module.
- Each plugin module depends on plugins.engine, which contains the main implementations of the api of the framework.
- Again, plugins.engine contains the implementation of the Information Plugin Framework’s APIs.
- The engine depends on other modules, like mpv, location and networking.
The weather and whereami modules are an example of how external modules contribute to Set or Map when using multibinding.
After looking at the code available in the final project for this chapter, you can successfully build and run the modularized version of the Busso App, getting what you see in Figure 15.8:
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
@Providesdefinitions in local@Modules. - You set a simple Kotlin interface as the dependency for a
@Componentand use it to pass information from the main app to a dependent module.
Congratulations! You’ve now created a modularized version of Busso, thereby completing the third section of this book. In the next section, you’ll learn everything you need to know about using Dagger in an Android app.