Dagger Series: How do you use it in your Modularization?

dependency-injection-modularized-android-application-1.jpg?w=2924

Story

I recently encountered an issue that I need to separate the App module (Android project) into several library modules to fulfill the requirements of the Dynamic Feature Module in the future. In the beginning, I thought that should be an easy job, how hard it can be?

wait-its-hard.gif
How hard it can be?

We need to move files to different library modules and solve the import path errors, change the implementation path in build.gradle file for different library modules. Then, it’s done easy.

So easy huh?

1*pPdeVsUU7F4o4tCeHqxgoA.gif
Too young too dumb

Unfortunately, if we need to use some 3rd-party libraries like Dagger to help us, the DI graph would be straightforward if we only have one App Module. If we only have one module, then we only need to create one component for this App Module. Once you move files to different library modules or dynamic feature modules (DFMs), there are two main problems you need to solve in this case:

  1. How do you create different components to let different modules provide resources for consumers?
  2. How do you offer different data for an injected object in a module at runtime?

I think the first issue would be much easier to solve. You have several weapons for this issue. In Dagger2, you can use either Component Dependency or Subcomponent to solve it.

What’s the difference between these two ways? Let’s see the scenario first.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Father {
@Inject lateinit var car: Car

fun drivingToOffice() {
car.go()
}
}

class Son {
@Inject lateinit var car: Car

fun drivingToSchool() {
car.go()
}
}

We have two classes Father and Son, and these two classes have the same injected object Car because Father owns the car and Son can borrow this car from his father. What the components look like if we use Dependency?

Dependency Way for Dagger Components

In the FatherComponent, we need to give it a customized scope, FatherScope, to differentiate from the SonComponent.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@FatherScope
@Component(
modules = [
AndroidInjectionModule::class,
CarModule::class
]
)
interface FatherComponent {

@Component.Builder
interface Builder {
@BindsInstance
fun application(application: FatherApplication): Builder

fun build(): FatherComponent
}
// provide car instance for son access
fun getCar(): Car
}

In the SonComponent:

1
2
3
4
5
6
7
8
9
10
11
@SonScope
@Component(
dependencies = [
FatherComponent::class
],
modules = [
// modules for son component
]
)
interface SonComponent {
}

You can see theSonComponent sets up a component dependency by including the FatherComponent; and FatherComponent exposes his car from CarModule via the function getCar(). By using component dependency, all classes which inject into SonComponent can use any resources exposed from FatherComponent.

But, there are two things you need to know about Component Dependency first:

  • You need to use different Scope for various components, cause they have different life cycles.

  • Child components can only use exposed resources from parent components.

SubComponent Way for Dagger Components

What about the Subcomponent way? Let’s see the code example first.

FatherComponent

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
@Module(
subcomponents = [
SonComponent::class
]
)
abstract class CarModule {
}

@FatherScope
@Component(
modules = [
AndroidInjectionModule::class,
CarModule::class
]
)
interface FatherComponent {

@Component.Builder
interface Builder {
@BindsInstance
fun application(application: FatherApplication): Builder

fun build(): FatherComponent
}
sonComponent(): SonComponent.Builder
}

SonComponent

1
2
3
4
5
6
7
8
9
10
11
12
13
@SonScope
@Component(
modules = [
// modules for son component
]
)
interface SonComponent {
@Component.Builder
@Subcomponent.Builder
interface Builder {
fun build(): SonComponent
}
}

There’re a few changes in these two components. First, you need to create a builder for SonComponent as well, which’s purpose is to create a SonComponent instance. And in the FatherComponent, we need to do two things:

  1. Let modules in FatherComponent know who the child component is. You will need to add the subcomponents keyword.
  2. Declare the SonComponent builder here to create your own SonComponent.

The way you use it would be like this:

1
2
3
4
val fatherComponent = DaggerFaComponent.builder()
.application(this)
.build()
val sonComponent = fatherComponent.sonComponent.build()

The example shows how we are going to solve the first question. Now, it’s time to go on the second question, how do we change module provides dynamically? Let’s use a configuration data class to solve this.

Dynamically change the content for the injected object

What’s the purpose of using the different content for an injected object at runtime? The possible use cases are running different environments when you are testing, especially when you have several debug modules with various base URLs running on different test servers for the Retrofit library. You would agree that creating different Retrofit instances is tedious. Let’s do only one Retrofit instance and apply for different base URLs.

First, create your own Configuration data class:

1
2
3
4
5
6
7
8
9
10
11
12
class Configuration (mode: Mode) {

enum class Mode {
Mode_1, Mode_2
}

var currentMode = Mode.Mode_1

init {
currentMode = mode
}
}

It’s a pure configuration class, and we use it to know what current mode is. So, how can we use it?

1*N29K_i5_14LAYG2TtP77aQ.jpeg
Inject it

Let’s use it as a resource provider in components.

1
2
3
4
5
6
7
8
9
10
11
12
@Module
abstract class ApplicatonModule {
@Binds
abstract fun bindApplicationContext(application: BaseApplication): Context

@Module
companion object {
@JvmStatic
@Provides
fun provideConfiguration(): Configuration = Configuration(Configuration.currentMode.Mode_1)
}
}

We can directly put it into the application-level module and provide it to the entire App when launching the App. Now you can inject this Configuration into where you want to use in this App. How do we give different URLs based on different mode when we are using Retrofit? In your module, you need to do something like this:

1
2
3
4
5
6
7
8
9
10
11
12
// In one of your modules:
@JvmStatic
@Provides
fun provideHttpClient(config: Configuration): OkHttpClient {
return ServiceFactory.getHttpClient(
BuildConfig.DEBUG, userAgent,
when (config.currentMode) {
Configuration.Mode.Mode_1 -> "server url 1"
Configuration.Mode.Mode_2 -> "server url 2"
}
)
}

We can create only one Retrofit with an OkHttpClient. Then, according to our configuration setting, we give it different URLs. Let me dive a little bit deeper into it. We can use Interceptor to change the URL we want to use based on the currentMode value in the Configuration class.

1
2
3
4
5
6
7
8
9
10
/**
* Provide Okhttp client
*/
fun getHttpClient(debugMode: Boolean, userAgent: String, url: String?): OkHttpClient {
return OkHttpClient.Builder()
/**
* for dynamically changing the url
*/
.addInterceptor(URLInterceptor(url))
}

URLInterceptor

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class URLInterceptor (private val url: String?): Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
val request = chain.request()

url?.let{
return chain.proceed(request
.newBuilder()
.url(url)
.build())
}

return chain.proceed(request)
}
}

Here we replace the old URL with a new one. Now we are all set. Let’s see how to use it:

1
2
3
4
5
6
7
8
9
class MainActivity: AppCompatActivity() {
@Inject lateinit var config: Configuration
override fun onCreate() {
super.onCreate()
// change here if you want
config.currentMode = Configuration.Mode.Mode_2
...
}
}

And now, we can dynamically change the runtime object content via this configuration object. Happy coding, enjoy.