Sharing Dagger 2 @Modules across multiple build targets in Android apps

In the discussion under my previous article on using Dagger 2 in multi-module Android applications, some questions arose about dealing with multiple build targets. That is, what if I want to build the same app in two versions, where most of the code is shared, but the other app has some extra features on top of it. What if I want to separate these features into separate modules?

There are multiple solutions to this problem, and picking the right solution for your specific needs will depend on your project’s particular needs. One of the simpler approaches might be to just use build flavors. This of course does not explain how the dependency injection graph will be shared across those flavors. We’ll leave it at that however, because it would look similar to what we’re going to do here.

Assumptions

Let’s say that not only you want to have a specific build flavor that contains all the features that the regular app does, but it also has some extra feature. Let’s call the basic version of the app app. And let’s call the app with the extra feature appb. Let’s also call this extra feature Feature B. Let’s further assume, that all features are separated by a gradle module and let’s also assume, that for some reason you really don’t want to depend on the Feature B module in the regular app build no matter what.

Reasonable solution

At such point, one of the reasonable solutions out there would be to have a gradle module for each build target. That means, to have modules app and appb. Now we can tell gradle in the appb build.gradle file, that we want to depend on module featureb, module that contains code of the Feature B. For simplicity of this article, let’s put all the shared code into common module.

Dagger 2

So how does Dagger 2 even come to the picture here? Truth be told, it really doesn’t. As I pointed out in the discussion under my previous article, this really has mostly to do with how files are being organized across multiple modules and as such, it’s mostly a gradle related exercise. That said, there is something to say about how to organise your Dagger 2 @Modules, such that you can share them across multiple gradle modules and be able to compose them.

Example

I’ve decided to create an example of such structure. You can find it in this Github repository: https://github.com/lukas1/DaggerSharedGraph

I’ll only post relevant dependency injection code here, you can view the rest of it in the repository.

Let’s first take a look at common module’s CommonModule . This is a Dagger 2 @Module , that contains necessary methods for building dependency injection graph for classes contained in common module, such as injector methods for Activities or Fragments. Let’s take a look:

@Module(includes = [
AndroidSupportInjectionModule::class
])
abstract class CommonModule {
@ActivityScope
@ContributesAndroidInjector()
abstract fun contributesCommonActivityInjector(): CommonActivity
}

In our simple example we only have one Activity called CommonActivity. We want this Activity to be accessible by both app and appb. If you don’t understand this code, please refer to my previous article, that explains this code in more detail.

We’ll add very similar code to module featureb, building code for dependency injection of classes in the featureb module:

@Module
abstract class FeatureBModule {
@ActivityScope
@ContributesAndroidInjector()
abstract fun contributesFeatureBActivityInjector(): FeatureBActivity
}

As I said, the two build targets are represented by separate modules: app and appb. We want to reuse CommonModule in both of our build targets app and appb.

App

Let’s start with app, since it’s the simpler of the two. As in most Dagger 2 applications, we’ll define AppComponent , that is starting point for dependency injection graph for our app. AppComponent is used to inject AndroidInjectors into our App class. This is how it will look like:

@Component(modules = [AppModule::class])
interface AppComponent: AndroidInjector<App>

We can see, that AppComponent uses AppModule to specify dependency injection rules for specific classes (such as dependencies that need to be provided using @Provides methods, methods for injecting Activities, etc).

Our AppModule in this example will be very simple:

@Module(includes = [
CommonModule::class
])
abstract class AppModule

In our app we want to be able to use CommonActivity (in fact in our example CommonActivity is set as a starting point of the app), therefore our AppModule must include CommonModule.

Note: Theoretically we don’t need AppModule and we could have included CommonModule directly in the AppComponent. This way however, if we’ll need to add app level dependencies that need to be provided using @Provides method, we’ll already have a good place to put them into.

Now we can use AppComponent in our App class to setup dependency injection for the whole app:

class App : DaggerApplication() {
override fun applicationInjector(): AndroidInjector<out DaggerApplication> = DaggerAppComponent.create()
}

(Here we’re taking advantage of DaggerApplication to reduce amount of boilerplate code, but it might not be the best solution, depending on your needs. For alternative, refer to my previous article)

App B

That was fairly straightforward. What about appb? What may not be so obvious is, that appb must have a separate dependency injection graph! Why you ask? Well, because there are some dependencies injected to appb, that are not present in app. Specifically we are talking about code contained in featureb module.

That means, our appb needs its own AppComponent, AppModule and even App class (Because App of app build target depends on completely different AppComponent). Therefore, we’ll get some duplicate code. App class for appb module will look almost the same, except for one import statement and so will AppComponent. The amount of duplicated code, however, is minuscule. If you need to share some code in the App class, you can always either inherit from some BaseApp, or inject to your App another class that handles what needs to be handled.

What is going to look different this time, however, is appb‘s AppModule class. Let’s take a look:

@Module(includes = [
CommonModule::class,
FeatureBModule::class
])
abstract class AppModule

Actually I lied a little bit. If you compare the two AppModule classes in this article, they’re almost identical. ALMOST. This one contains one extra line. It includes FeatureBModule in the list of included @Modules. Remember how we wanted our appb to have all the features that app has, but include Feature B on top of that. Well, that’s it. Pretty anticlimactic, right? :-)

Wrapping up / TL;DR

This was actually fairly straightforward, no extremely difficult workaround effort is needed to have multiple build targets with shared code, but some different extra features in regards to Dagger 2. You just need have separate dependency injection graphs for each build target, then create a @Module that contains dependency injection instructions for all the shared code, have a @Module for each extra feature and then just combine them as you need in every build target’s AppModule. This approach will work even if you decide to go the route of build flavors in one app module, just the file structure will look little bit different (and you’ll be able to get rid of most of the duplicate code, but your app module will then depend on every other module in the code - a trade-off that you must evaluate for yourself).