This is not a tutorial on Hilt.
It’s also not a tutorial on Dagger subcomponents. Instead, I want to present a surprising (to me) thing I learned recently about them. I guess you can think of this as documentation for a future me, inevitably confused by subcomponents, and searching for an answer. I hope that this is also useful to others.
I joined my present team about eight months ago. And about six months ago, I had already become so infuriated at the way we were doing dependency injection (DI) that I took a weekend to rewrite all our Dagger-related code from scratch. In doing so, I made a small error which I only began to understand six months later (i.e., last week).
No, I haven’t been fired yet.
My approach to DI follows closely from my approach to software organization in general; that is, many small modules, isolated from each other and independently evolvable (while also following an API contract), and also relatively easily testable.1 This has led to a DI architecture that looks like (simplified)
A parent, “application” or singleton-scoped component, followed by many activity-scoped subcomponents, one per Activity
. Fragments are also injected from their parent activities.
In code, this looked like:
@Singleton
@Component(modules = {
ApplicationSubcomponentsModule.class
})
public interface ApplicationComponent {
MainActivitySubcomponent.Factory mainActivityFactory();
}
Dagger experts will already see the issue, but don’t worry, we’ll get there.
The activity subcomponent looks like
@ActivityScope
@Subcomponent
public interface MainActivitySubcomponent {
void inject(MainActivity a);
void inject(MainFragment f);
@Subcomponent.Factory interface Factory {
MainActivitySubcomponent newSubcomponent();
}
}
and finally, the activity injects with
public final class MainActivity extends Activity {
@Override public void onCreate(Bundle savedInstanceState) {
getApplicationComponent()
.mainActivityFactory()
.newSubcomponent()
.inject(this);
super.onCreate(savedInstanceState);
}
}
where getApplicationComponent()
is left as an exercise, and with the understanding that the whole statement can be vastly simplified with some helper classes.
There are two ways of linking subcomponents to parent (sub)components
Before we go on, I need to add one more piece to the puzzle, which was left out deliberately a moment ago. What is ApplicationSubcomponentsModule
?
@Module(subcomponents = {
MainActivitySubcomponent.class
})
public interface ActivitySubcomponentsModule {
}
It is a Dagger @Module
which points to a list of @Subcomponent
s which are installed on the parent (sub)component. I will refer to this method of installing a subcomponent as the “Module.subcomponents()
approach.” This is the first way of linking a subcomponent with a parent. What’s the second? Well, that’s this line
MainActivitySubcomponent.Factory mainActivityFactory();
which specifically enables
getApplicationComponent()
.mainActivityFactory()
.newSubcomponent()
.inject(this);
I will refer to this as the “factory method approach.”
I can now finally tell you the surprising thing I learned: you don’t need both approaches to associate a subcomponent with a parent. I learned this shocking fact when I noticed that I had failed to add a new activity’s subcomponent module into the list at ActivitySubcomponentsModule
, and my code still worked. The shock compounded when I saw this wasn’t the first case of someone forgetting to do this (and I, uh, actually did code review on that first one).
This confusion was the result of a misreading of the Dagger documentation (or maybe bad documentation, but I’m comfortable blaming myself for this). If you look at the javadoc on the @Component
annotation, you’ll see this:
Subcomponents are declared by listing the class in the
Module.subcomponents()
attribute of one of the parent component's modules. This binds theSubcomponent.Builder
orSubcomponent.Factory
for that subcomponent within the parent component.Subcomponents may also be declared via a factory method on a parent component or subcomponent.
The emphasis there is mine. I missed that the first time, and thought both approaches were required to install a component. Turns out, you can do one or the other. There is a difference, however.
A tale of two approaches
With the factory method approach, as we’ve already seen, you can write
getApplicationComponent()
.mainActivityFactory()
.newSubcomponent()
.inject(this);
and this is often very convenient. It also has the non-trivial benefit of being much easier to maintain and understand than the Module.subcomponents()
approach (a module which lists subcomponents that are “installed” on a parent).
What is enabled by Module.subcomponents()
? Well, it’s actually entirely redundant if you’re also using the factory method approach. But if you’re only using this, here’s what you have available to you:
public class Thing {
private final SomeSubcomponent subcomponent;
@Inject
public Thing(Provider<SomeSubcomponent.Factory> factoryProvider) {
subcomponent = factoryProvider.get().newSubcomponent();
}
}
which I guess is neat.
tl;dr
Use the factory method approach. It’s easier to write, maintain, understand, and use. It will also let you inject factory providers if you want to ¯\_(ツ)_/¯
Endnotes
1 As easy as it ever is to write and run instrumented tests on Android.
Top comments (0)