Dagger2 and Multi-Module Android Applications

It seems it’s still not easy for many Android developers to figure out how to use Dagger2 for dependency injection in a multi-module application.

Sure, if you’re keeping all your activities in the main module, then you don’t have a problem. That’s not the most natural way of separating code into modules, however. In most cases you want to keep activities related to a feature with the rest of that feature code, in the same module.

Assumptions

In this article I’m going to assume that you’re familiar with Dagger 2, specifically that you understand what is a @Component, a @Module and Scope.

Why traditional approaches don’t work here

In traditional approaches to dependency injection with Dagger 2, it was usual to have some sort of Injector class, then call something like Injector.component.inject(this) from every Activity . The issue with this approach in multi-module project is, that your Dagger2 components need to know about the Activity and the Activity needs to know about the Dagger2 component. This, of course, creates a circular dependency. In a single module application, this is not an issue. When you split your project into multiple modules, however, circular dependencies across modules are not possible with Gradle projects.

The hard way

This issue can be resolved with just Dagger2 by using @IntoMap annotated @Provide methods in Dagger2 @Module classes, and lot of other boilerplate code. I’m not going to present this approach, because it’s rather tedious. That said, this is the base of what’s happening under the hood of presented solution, so if you’re interested in knowing more, I highly recommend watching this talk by Greg Kick, especially the part Easier subcomponents: https://www.youtube.com/watch?v=iwjXqRlEevg (Easier subcomponents part starts at around 28:10)

Dagger.Android

The easiest way to resolve this issue is to use dagger.android , official library from Google, that packs some special classes and annotations to support easier dependency injection for Android apps. As I mentioned before, what these classes, interfaces and annotations do under the hood is, that they are generating code similar to what has been described by Greg Kick in his talk linked above.

Without further ado, let’s look at how to setup dependency injection using Dagger 2 in multi-module projects.

Adding gradle dependencies

In our example, using Kotlin, we’ll have two modules: app and module . We’ll first need to add dependency on Dagger 2 to our build.gradle of app module:

// Dagger Dependencies
implementation 'com.google.dagger:dagger:2.18'
kapt 'com.google.dagger:dagger-compiler:2.18'

// Dagger.android Dependencies
implementation 'com.google.dagger:dagger-android:2.18'
implementation 'com.google.dagger:dagger-android-support:2.18' // if you use the support libraries
kapt 'com.google.dagger:dagger-android-processor:2.18'

Also, since we’re using Kotlin and we need annotation processing for Dagger 2, we need to also apply kapt plugin, at the top of app module’s build.gradle file:

apply plugin: 'kotlin-kapt'

Since we’re going to use dagger.android annotations and classes in our module module as well, we need to add these dependencies to build.gradle of the module :

// Dagger Dependencies
implementation 'com.google.dagger:dagger:2.18'

// Dagger.android Dependencies
implementation 'com.google.dagger:dagger-android:2.18'

All set and done.

Scope, Module, Component

Every Android application using Dagger 2 for dependency injection needs to specify at least one @Component and in most cases at least one @Module . In most cases we also want to specify scope for the lifetime of activity. This means, that dependencies injected into activity will live as long as the activity is alive.

Let’s create our ActivityScope first and place it into the app module:

@Scope
annotation class ActivityScope

With that done, we’ll move to writing a @Module. First, let’s suppose we have two activities in our project: MainActivity and ModuleActivity . Let’s put our MainActivity to app module and ModuleActivity into module . Our @Module placed within app module will then look something like this:

@Module(includes = [
AndroidSupportInjectionModule::class
])
abstract class AppModule {
@ActivityScope @ContributesAndroidInjector()
abstract fun contributesMainActivityInjector(): MainActivity

@ActivityScope @ContributesAndroidInjector()
abstract fun contributesModuleActivityInjector(): ModuleActivity
}

What’s going on here? First, let’s look at the methods in our AppModule. We can see:

@ActivityScope @ContributesAndroidInjector()
abstract fun contributesMainActivityInjector(): MainActivity

This says, in simpler terms, that we want to enable injecting dependencies into MainActivity and all injected dependencies should stay alive as long, as the activity is still around. Crucial part for this to work is the @ContributesAndroidInjector() annotation. This annotation comes from the dagger.android library. We’ll get back to it later in the article.

When you need to add other activities and want to be able to inject dependencies into them, just list them in this module just like MainActivity and ModuleActivity .

Another important part of setting up this module, is including AndroidSupportInjectionModule . Thanks to this module, Dagger 2 will generate code that is necessary for AndroidInjection, that we’re going to use later, to work. We’ll reveal a little bit more about its purpose later in the article.

Now AppModule is ready to be used in our main application component. We’ll call it AppComponent. This is, how it will look like:

@Component(modules = [AppModule::class])
interface AppComponent {
fun inject(app: App)
}

Theoretically, your AppComponent could implement AndroidInjector interface from dagger.android, but this will look more familiar to developers experienced with tradition approaches to dependency injection using Dagger 2, so that’s why I decided to do it this way for this particular example.

Extending Application class

To enable using AndroidInjection within our activities, one more step is needed. AndroidInjection needs to have access to list of injectable activities. This will be achieved by injecting DispatchingAndroidInjector<Activity> to an extended Application class. Thanks to this class, activities don’t have to depend directly on app module and Application class doesn’t need to know anything about specific activities.

We’ll need to create our own Application class:

class App : Application(), HasActivityInjector {
@Inject lateinit var dispatchingActivityInjector: DispatchingAndroidInjector<Activity>

override fun onCreate() {
super.onCreate()
DaggerAppComponent.create().inject(this)
}

override fun activityInjector(): AndroidInjector<Activity> = dispatchingActivityInjector
}

App class implements HasActivityInjector interface, that is needed for internal purposes of AndroidInjection . App class could extend from DaggerApplication and then implementing HasActivityInjector would be unnecessary, but I personally don’t like to inherit from library classes if it can be avoided. (What if our App needed to also inherit from Application of another library and it couldn’t be avoided?)

In onCreate() method we invoke inject() method of AppComponent , so that dispatchingActivityInjector is injected into our App. This is the reason, why we had to include AndroidSupportInjectionModule. This module contains code that will provide DispatchingAndroidInjector<Activity> to our App.

Final step

Now we can finally inject into our activities. Let’s have a class that we want to inject to some activity:

class ModuleActivityDependency @Inject constructor() {
val text = "Hello World from Module Activity Dependency"
}

Let’s put that class into module module and let’s inject it into ModuleActivity (also located in module):

class ModuleActivity : AppCompatActivity() {

@Inject lateinit var dependency: ModuleActivityDependency

override fun onCreate(savedInstanceState: Bundle?) {
AndroidInjection.inject(this)
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_module)

textViewModuleActivity.text = dependency.text
}
}

Important part here is this line: AndroidInjection.inject(this) . This line should be called before super.onCreate() in onCreate() method of every Activity where you want to use dependency injection. Notice, that there’s no direct dependency on AppComponent , AppModule nor App . Our activity has no dependencies on classes from app module.

What have we even done here?

At the core of this solution, there are several components:

AndroidSupportInjectionModule is a dagger @Module, that will provide instance of DispatchingAndroidInjector<Activity> . You need to inject it to your Application class.

Your Application class also needs to implement HasActivityInjector interface. This interface will force you to implement method fun activityInjector(): AndroidInjector<Activity>. You need to return here the injected instance ofDispatchingAndroidInjector<Activity> .

DispatchingAndroidInjector<Activity> contains a map of injectors for activities. To put your own Activity into this map, you must create a method for it in a @Module and mark it with @ContributesAndroidInjector() annotation.

In every Activity that needs dependency injection, call AndroidInjection.inject() . This method will get instance of Application and it will check if it implements HasActivityInjector. It will invoke fun activityInjector(): AndroidInjector<Activity> to get instance of DispatchingAndroidInjector<Activity> , then it will look for injector for your activity in the map of injectors stored in DispatchingAndroidInjector<Activity> .

Summary

And that’s it. Admittedly, it’s not the most straightforward process. It’s a price to be paid for dependency injection in multi-module architecture. If you had trouble following this article, perhaps looking directly into code will help. You can check out this example project, that I wrote just for this article:
https://github.com/lukas1/DaggerMultiModule

Bonus: What about Fragments?

We’ve avoided discussing fragments in this article. For the most part, it’s similar to injecting activities. There is one extra step that needs to be taken to enable injection of Fragments. Your Application must implement HasSupportFragmentInjector (or inherit from DaggerApplication). Also, AndroidInjection.inject() should not be called from onCreate() , rather from onAttach() . More details can be found in official Dagger 2 documentation: https://google.github.io/dagger/android