DEV Community

Tony Robalik
Tony Robalik

Posted on

Bare Metal Dagger: Wiring Subcomponents

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)

Simplified entity diagram

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();
}
Enter fullscreen mode Exit fullscreen mode

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();
  }
}
Enter fullscreen mode Exit fullscreen mode

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);  
  }
}
Enter fullscreen mode Exit fullscreen mode

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 {
}
Enter fullscreen mode Exit fullscreen mode

It is a Dagger @Module which points to a list of @Subcomponents 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();
Enter fullscreen mode Exit fullscreen mode

which specifically enables

getApplicationComponent()
  .mainActivityFactory()
  .newSubcomponent()
  .inject(this);
Enter fullscreen mode Exit fullscreen mode

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 the Subcomponent.Builder or Subcomponent.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);
Enter fullscreen mode Exit fullscreen mode

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();
  }
}
Enter fullscreen mode Exit fullscreen mode

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)