GitHub LinkedIn Feed

Dagger 2: Even Sharper, Less Square

May 4, 2015
Events
Android Australia UG Meetup — May 5, 2015
Table of Contents

It was only a matter of time before version 2.0 of the well-known dependency injection library Dagger hit production and that seemed like a good reason to write an article about it. But first we’ll spend some time looking at the original Dagger.

Dagger 1

As a brief history lesson, Dagger was created back in 2012 by a few good developers at Square, who liked the idea of dependency injection in Java but found Guice (standard at the time) to be a tad slow for their liking. As a result, their offering relied on annotation-based code generation (JSR-330) and had an API similar to that of Guice, but provided better performance and more flexibility.

The way Dagger works is by defining modules that contain provider methods for all the dependencies that you might want to inject, loading these modules into an object graph and finally injecting its contents into targets as needed. Simple enough structure (not so simple in implementation, of course) helps developers decouple their code and cut through the ugly factory-filled instantiation lines at the top of each class by moving them into the injectors generated by the library.

However, together with the apparent advantages (of which there were enough to make Dagger one of the first libraries that an average Android developer would import into their new projects), there were apparent problems that could not be fixed with a quick pull request or two, because they were with the underlying architecture:

  • Graph composition at runtime — hurts performance, especially in a per-request use case (backend scenario)
  • Reflection (i.e. Class.forName() on generated types) — makes generated code hard to follow and ProGuard a nightmare to configure
  • Ugly generated code — especially, in comparison to similarly written manual instantiation from factories

As a result, any issues raised in the tracker directly or indirectly related to the above were marked as “it is what it is, will fix in Dagger 2”.

Dagger 2

Fast-forward to present day, together with the original creators at Square, the core libraries team at Google (creators or Guava) brought out the next iteration of Dagger, which hit production just under a month ago.

The new release, as promised, addresses many of the problems of the original:

  • No more reflection — everything is done as concrete calls (ProGuard works with no configuration at all)
  • No more runtime graph composition — improves performance, including the per-request cases (around 13% faster in Google’s search products, according to Gregory Kick)
  • Traceable — better generated code and no reflection help make the code readable and easy to follow

Usage

Although Dagger works with any Java projects, the code samples will focus specifically on Android. However, none of them should be hard to convert to anything, say, backend-related.

Module

The part that is largely unchanged in Dagger 2 is the module. You would still define provider methods for any injectable dependencies. Say, we want to inject the SharedPreferences object:

@Module
public class ApplicationModule {
    private Application mApp;

    public ApplicationModule(Application app) {
        mApp = app;
    }

    @Provides
    @Singleton
    SharedPreferences provideSharedPrefs() {
        return PreferenceManager.getDefaultSharedPreferences(mApp);
    }
}

Similarly, you can provide anything else (not an exhaustive list, just some ideas):

  • Context
  • System services (e.g. LocationManager)
  • REST service (e.g. Retrofit)
  • Database manager (e.g. Realm)
  • Message passing (e.g. EventBus)
  • Analytics tracker (e.g. Google Analytics)

Component

The eagle-eyed readers would have noticed the absence of the injects = {} parameter in the @Module annotation — that is because Dagger 2 no longer expects it. Instead, the concept of “components” is used to associate modules with injection targets.

@Singleton
@Component(modules = {ApplicationModule.class})
public interface ApplicationComponent {
    void inject(DemoApplication app);
    void inject(MainActivity activity);
}

As a result, you still need to define all your injection targets, except now forgetting to do so results in a simple “cannot find method” compile error, rather than a cryptic runtime one.

Application

Next stop is the container for your component. Depending on your application, you may choose to store your components in more complicated ways, but for the sake of this example, a single component stored in the application class will suffice.

public class DemoApplication extends Application {
    private ApplicationComponent mComponent;

    @Override
    public void onCreate() {
        super.onCreate();
        mComponent = DaggerApplicationComponent.builder()
                    .applicationModule(new ApplicationModule(this))
                    .build();
    }

    public ApplicationComponent getComponent() {
        return mComponent;
    }
}

Nothing particularly new here: instead of the usual ObjectGraph, you build and instantiate the corresponding component.

Note: Chances are you will wonder about this at some point, so I will explain ahead of time. The DaggerApplicationComponent in the example above is a generated class (named as Dagger%COMPONENT_NAME%), which only appears once the project has been rebuilt since the last changes to the component. So if you are not finding it, hit “Rebuild Project” in your IDE and ensure that you don’t have any compile errors. Similarly, applicationModule() method corresponds to the name of your module.

Injection

Finally, we come to using the injection setup that we put together in the previous sections, in an activity that needs access to the SharedPreferences.

public class MainActivity extends Activity {
    @Inject SharedPreferences mSharedPrefs;

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        ((DemoApplication) getApplication())
                    .getComponent()
                    .inject(this);

        mSharedPrefs.edit()
                    .putString("status", "success!")
                    .apply();
    }
}

As expected, nothing has changed in the @Inject annotation and little has changed in the injection itself.

There is a slight inconvenience related to the strong type association with the inject() method but I will discuss that later.

Named Injections

Suppose you have multiple objects of the same type, e.g. two different SharedPreferences instances (potentially, pointing to different files). What can you do then?

This is where named injections come in. Simply annotate the providers in your module with @Named as follows:

@Provides
@Named("default")
SharedPreferences provideDefaultSharedPrefs() {  }

@Provides
@Named("secret")
SharedPreferences provideSecretSharedPrefs() {  }

And do the same in the injection target:

@Inject @Named("default") SharedPreferences mDefaultSharedPrefs;
@Inject @Named("secret") SharedPreferences mSecretSharedPrefs;

That’s it! Not much else to explain, but I figured it would be a useful thing to mention.

Lazy Injections

Talking about performance, let’s say there are many different injections in one target and some of them will only be used if some event occurs that doesn’t always occur (e.g. user input). It feels like a waste to always inject that dependency, so instead we can do a lazy injection:

@Inject Lazy<SharedPreferences> mLazySharedPrefs;

void onSaveBtnClicked() {
    mLazySharedPrefs.get()
                .edit().putString("status", "lazy...")
                .apply();
}

That means mLazySharedPrefs will not actually get injected until the first call to get(). From then on it will remain injected regardless of how many times it gets called after that.

Provider Injections

The last trick that I wanted to mention comes with a warning that if you find yourself doing this in a clean new project that you have complete control of, you may need to consider other alternatives, such as the factory pattern. However, for legacy code this could be a lifesaver.

Consider a situation where you need to create multiple instances of an object, instead of just injecting one. For that situation you can inject a Provider<T>:

@Inject
Provider<Entry> mEntryProvider;

Entry entry1 = mEntryProvider.get();
Entry entry2 = mEntryProvider.get();

In this example, your provider would create two different instances of the Entry object. One thing to note is that it is up to you to define how the instances are created in the module.

Annoyances

It’s a good sign that I really couldn’t bring myself to call this section “Problems”, because none of them qualify as anything more than annoyances.

Annoyance #1: As mentioned before, the inject() method now has a strong type association with the injection target. This is good for debugging, but it complicates a common practice of injecting from base classes (e.g. base activities, fragments etc).

Intuitively, you could try creating an inject() method for for base class, however that would only inject the dependencies defined in that class, not its subclasses. Solution #1: The solution that I use is defining an abstract method in your base class to perform the actual injection:

@Override
public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    injectComponent(((DemoApplication) getApplication())
                .getComponent());
}

protected void injectComponent(ApplicationComponent component);

And then implement it in your subclasses:

@Override
protected void injectComponent(ApplicationComponent component) {
    component.inject(this);
}

An additional method compared to none in Dagger 1 — yes, but well worth the reward of compile-time checking in my opinion.

Solution #2: The other option is to use reflection. If you lost interest upon reading that first sentence, I can’t blame you: it was one of the points of Dagger 2 to not use reflection. However, if you are set on injecting in base classes without extra methods — read on.

This solution boils down to finding the right inject() method for a type that you are dealing with.

First, you would cache all the methods defined in your component class against their parameter types (since inject() can only contain one, you only care about the first one):

// Keep the returned cache in some helper.
Map<Class, Method> buildCache() {
    Map<Class, Method> cache = new HashMap<>();
    for (Method m : ApplicationComponent.class.getDeclaredMethods()) {
        Class[] types = m.getParameterTypes();
        if (types.length == 1) {
            cache.put(types[0], m);
        }
    }
    return cache;
}

Finally, you would call getClass() on the injection target, find the corresponding method in your cache and call invoke() on it.

// Use for injecting targets of any type.
void inject(ApplicationComponent component, Object target) {
    Method m = cache.get(target.getClass());
    if (m != null) {
        m.invoke(component, target);
    }
}

I will reiterate that this approach is over-engineering the flow only to save a few lines and I would personally not use it, but somebody who either wants to hide all their setup in the base class or is dealing with legacy code that already does this, might find it useful.

Annoyance #2: Component implementation (e.g. DaggerApplicationComponent) requires rebuilding the project to appear and any injection-related compile errors result in the class disappearing (i.e. not being generated).

Not a serious annoyance, I admit, but you may find it irritating, at least in the first hour working with Dagger 2, especially since this is the time when your components will change often. Hence, it’s worth a mention.

Solution: None that I know of.

Conclusion

Not feeling like writing a lengthy conclusion, so I shall keep it short and sweet.

Is Dagger 2 a significant improvement over the original? Yes, very much so.

Are there problems? Yes, but none serious enough to keep you from trying it out and even migrating your production projects to.

Source

Resources

  • android
  • mobile
  • java
  • talk