Dagger in a multi-module project without dagger.android
Setting up Dagger in a multi-module project is non-trivial. Usual solution I’ve seen is to use dagger.android. I dare to say I know my way around Dagger but the android library never clicked for me, as it seemed too magical. So didn’t bother using it. And it seems it was the right choice as Google seems to abandon it.
Basics
The pragmatic way of doing things in a single module app in my opinion is to have AppComponent
with @AppScope
’d dependencies. AppComponent
instance lives in Application subclass, and you access it via casting ApplicationContext
. I also use (Sub)Components per feature, to inject ViewModel
s. But I’m sure having them in a single module and use assisted injection etc., is fine also. This is pretty standard and common setup.
Where this breaks down, is in a multi-module (gradle) setup. Issue is the cyclical reference nature of AppComponent
and the call site inside of the feature module.
Obviously there are many ways to modularize, but in my opinion the most basic and scalable is modularizing by feature. That yields a diamond shaped module graph, with :base
module for shared code, bunch of :featureN
which depend on :base
, and at the tip :app
module which depends on features, containing only minimal code.
However now in a feature, given the direction of module dependencies, there is no way to access the AppComponent.
So we need to jump some hoops. Luckily Dagger allows the @Component annotated interface to extend some other interface. This is the key.
Create the base AppComponent interface in :base
(note that it’s a plain interface, no dagger annotation). Now create the AppComponentImpl
in :app
and slap the @Component
annotation on it
Instantiate the component like this
And feature looks like this
Note that in the sample I use Conductor’s Controller instead Fragment, and the ViewModel is a simple interface, not necessarily the Jetpack’ one. But it’s the same concept.
This works and is relatively straight forward. However doesn’t allow for breaking apart :base module, which will grow over time and will be your bottleneck in compile times. So we want to break :base apart. Don’t do this prematurely as it complicates things, but you should have a direct line of sight as how to do it, when needed.
Breaking :base apart
Okay, let’s say we have a MessageRepository
. It’s a singleton, you only want one instance per app. MesageRepository
is used by :feature1
and :feature2
.
Let’s also say it becomes too big, you break it’s implementation into several classes, or you want to share this logic with some other project you’re doing i.e. it warrants its own :messages
module. So we extract the MessageRepository
from :base
and move it into the new :messages
module.
The issue is, how to attach MessageRepository
to the AppComponent
, which lives in :base
, where :base
has no knowledge of :messages
while still keeping it singleton.
First, :messages
should expose a public @Module
(or use @Inject
constructor if you’re that kind of a person).
Second, let’s again assume our Feature1ControllerComponent
which provides Feature1ViewModel
Since now, MessageRepository
is no longer present in the AppComponent
definition, how should you pass in the dependencies parameter?
Solution
The trick with Dagger is that @Component(dependencies=[…]) allows to take any random interface, not just @Component annotated ones! The interface just has to have getters which match with the dependency types our module needs. I wish somebody told me this earlier
So, let’s declare a feature module local “partial” app component
Now let the AppComponentImpl
extend it and attach the MessageModule
.
Since FeatureAppComponent
extends AppComponent
you can access the instance again via getting the application context, casting it to AppComponentProvider
and casting the appComponent()
to Feature1AppComponent
.
That’s it. It is quite to a lot to digest, but at least there’s no magic involved.
Thanks for reading!
Inspired by Uber RIBs and Ben Schwab’s great talk