/ ANDROID, DAGGER, DEPENDENCY INJECTION

Compile-time dependency injection tradeoffs in Android

As a backend software developer, I’m used to Spring as my favorite Dependency Injection engine. Alternatives include Java EE’s CDI which achieves the same result - in a different way. However, both inject at runtime: that means that there’s a definite performance cost to pay at the start of the application, the time it takes for all dependencies to be fulfilled. On an application server, where the application lifespan is measured in days (if not weeks), the start time overhead is acceptable. It is even fully transparent if the server is but a node in a large cluster.

As an Android user, I’m not happy when I start an app and it lags for several seconds before opening. It would be very bad in term of user-friendliness if we were to add several more seconds to that time. Even worse, the memory consumption from a DI engine would be a disaster. That’s the reason why Square developed a compile-time dependency injection mechanism called Dagger. Note that Dagger 2 is currently under development by Google. Before going further, I must admit that the documentation of Dagger 2 is succinct - at best. But it’s a great opportunity for another blog post :-)

Dagger 2 works with the annotation-processor: when compiling, it will analyze your annotated-code and produce the wiring code between you components. The good thing is that this code is pretty similar to what you would write yourself if you were to do it manually, there’s no secret black magic (as opposed to runtime DI and their proxies). The following code displays a class to be injected:

public class TimeSetListener implements TimePickerDialog.OnTimeSetListener {

    private final EventBus eventBus;

    public TimeSetListener(EventBus eventBus) {
        this.eventBus = eventBus;
    }

    @Override
    public void onTimeSet(TimePicker view, int hourOfDay, int minute) {
        eventBus.post(new TimeSetEvent(hourOfDay, minute));
    }
}

Notice the code is completely independent of Dagger in every way. One cannot infer how it will be injected in the end. The interesting part is how to use Dagger to inject the required eventBus dependency. There are two steps:

1. Get a reference to an eventBus instance in the context 1. Call the constructor with the relevant parameter

The wiring configuration itself is done in a so-called module:

@Module

public class ApplicationModule {

    @Provides
    @Singleton
    public TimeSetListener timeSetListener(EventBus eventBus) {
        return new TimeSetListener(eventBus());
    }

    ...
}

Notice that the EventBus is passed as a parameter to the method, and it’s up to the context to provide it. Also, the scope is explicitly @Singleton.

The binding to the factory occurs in a component, which references the required module (or more):

@Component(modules = ApplicationModule.class)
@Singleton
public interface ApplicationComponent {

    TimeSetListener timeListener();

    ...
}

It’s quite straightforward…​ until one notices that some - if not most objects in Android have a lifecycle managed by Android itself, with no call to our injection-friendly constructor. Activities are such objects: they are instantiated and launched by the framework. Only through dedicated lifecycle methods like onCreate() can we hook our code into the object. This use-case looks much worse as field injection is mandatory. Worse, it is also required to call Dagger: in this case, it acts as a plain factory.

public class EditTaskActivity extends AbstractTaskActivity {

    @Inject TimeSetListener timeListener;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        DaggerApplicationComponent.create().inject(this);
    }

    ...
}

For the first time we see a coupling to Dagger, but it’s a big one. What is DaggerApplicationComponent? An implementation of the former ApplicationComponent, as well as a factory to provide instances of them. And since it doesn’t provide an inject() method, we have to declare it into our interface:

@Component(modules = ApplicationModule.class)
@Singleton

public interface ApplicationComponent {

    TimeSetListener timeListener();

    void inject(EditTaskActivity editTaskActivity);

    ...
}

For the record, the generated class looks like:

@Generated("dagger.internal.codegen.ComponentProcessor")
public final class DaggerApplicationComponent implements ApplicationComponent {

  private Provider<TimeSetListener> timeSetListenerProvider;
  private MembersInjector<EditTaskActivity> editTaskActivityMembersInjector;

  ...

  private DaggerApplicationComponent(Builder builder) {
    assert builder != null;
    initialize(builder);
  }

  public static Builder builder() {
    return new Builder();
  }

  public static ApplicationComponent create() {
    return builder().build();
  }

  private void initialize(final Builder builder) {
    this.timeSetListenerProvider = ScopedProvider.create(ApplicationModule_TimeSetListenerFactory
        .create(builder.applicationModule, eventBusProvider));
    this.editTaskActivityMembersInjector = TimeSetListener_MembersInjector
        .create((MembersInjector) MembersInjectors.noOp(), timeSetListenerProvider);
  }

  @Override
  public EventBus eventBus() {
    return eventBusProvider.get();
  }

  @Override
  public void inject(EditTaskActivity editTaskActivity) {
    editTaskActivityMembersInjector.injectMembers(editTaskActivity);
  }

  public static final class Builder {

    private ApplicationModule applicationModule;

    private Builder() { }

    public ApplicationComponent build() {
      if (applicationModule == null) {
        this.applicationModule = new ApplicationModule();
      }
      return new DaggerApplicationComponent(this);
    }

    public Builder applicationModule(ApplicationModule applicationModule) {
      if (applicationModule == null) {
        throw new NullPointerException("applicationModule");
      }

      this.applicationModule = applicationModule;
      return this;
    }
  }
}

There’s no such thing as a free lunch. Despite compile-time DI being very appealing at first glance, it becomes much less so when used on objects whose lifecycle is not managed by our code. The downsides become apparent: coupling to the DI framework and more importantly an increased difficulty to unit-test the class. However, considering Android constraints, this might be the best that can be achieved.

Nicolas Fränkel

Nicolas Fränkel

Developer Advocate with 15+ years experience consulting for many different customers, in a wide range of contexts (such as telecoms, banking, insurances, large retail and public sector). Usually working on Java/Java EE and Spring technologies, but with focused interests like Rich Internet Applications, Testing, CI/CD and DevOps. Also double as a trainer and triples as a book author.

Read More
Compile-time dependency injection tradeoffs in Android
Share this