A Friendly Introduction to Dagger 2: Part 1

What is dependency injection, what is Dagger, and how can they help us write cleaner, more testable code.

Dependency injection is a great technique to increase the testability of an application and Dagger 2 is one of the most popular dependency injection frameworks currently available for Java and Android. Most introductory guides to Dagger 2, however, assume that the reader is already deeply familiar with dependency injection and its heavy terminology, making it hard for newcomers to jump on board.

In this series of posts, I will try to give you a more friendly introduction to Dagger 2, with plenty of complete code samples that you can actually compile and modify.

To keep things practical and concise, I have deliberately left out all the history of dependency injection frameworks, as well as the endless variations. We will start with one specific flavor of dependency injection (constructor injection) and visit other variations as we move along.

So, what is dependency injection?

Dependency injection is a technique that makes classes more easy to test and to reuse. To see how can we apply it, let’s start with an example. Suppose that we need to write an application that prints the current weather conditions on the console. A naive implementation could look like this:

Some methods have been omitted, since they are not important. The focus here is that, in order to perform its job, a WeatherReporter needs two other objects: a LocationManager, which can detect the current user’s location, and a WeatherService, which can provide the temperature for any given location.

In a well designed object-oriented application, each object has only a very small number of responsibilities, and rely on other objects to accomplish most of the work. These other objects are called dependencies. Before an object can start doing real work, all of its dependencies must be satisfied somehow. The dependencies for our WeatherReporter, for example, are satisfied by creating new instances of them on the constructor.

While instantiating dependencies on the constructor works well enough for small applications, it comes with a number of shortcomings as the application evolves. First, it makes the class rather inflexible. If this app is supposed to run on multiple platforms, for example, we might want to swap a certain LocationManager by another, but this cannot be easily done. Perhaps, we would also like to share the same LocationManager among multiple objects, but this is also hard to accomplish, unless the class is modified.

Secondly, it is impossible to test our class in isolation. Constructing a single WeatherReporter will always automatically construct two other objects, which will end up getting tested along with the original object. This can be a serious problem if one of these dependencies relies on an expensive external resource such as a network connection, or if it has a large number of dependencies itself.

Both problems arise because our class has two distinct responsibilities. It must not only know how to perform a certain task, but also where to find the collaborators it needs to accomplish it. If, instead, we provide to our object everything it needs to perform its job, then the previous problems disappear. The class can easily work with different collaborators and can be tested in isolation.

This approach is called dependency injection. In an application that relies on dependency injection, the objects never have to hunt around for dependencies or construct them themselves. All the dependencies are provided to them, or injected into them, ready to be used.

Building the graph

At some point, of course, someone has to instantiate the dependencies and actually provide them to the objects that might require them. This phase, called building the dependency graph, is usually done at the entry point of the application. On the desktop, for example, this code would be found inside the main method, as shown below. On Android, it could be done inside an Activity’s onCreate method.

If the project is as simple as our previous example, then instantiating and injecting a few dependencies manually on the main method is very reasonable. Most projects, however, have dozens of classes, each with various dependencies that need to be satisfied. Instantiating and wiring everything together requires a lot of code. Even worse, this code will constantly have to change every time a new class is added to the application, or any time an existing class is modified to require a new dependency.

To illustrate this problem, let’s make our small example a little more realistic. In practice, a WeatherService would probably require, say, a WebSocket to communicate over the network. A LocationManager would require a GPSProvider to communicate with the hardware, and most classes could also require a Logger, to print useful debugging information on the console. The modified main function would look like this:

Very quickly, the entry point for our application has started to become bloated with initialization code. To build a single WeatherReporter, which is the object we actually care about, we had to manually instantiate numerous others. As the application grows and more classes are added, this main method will keep growing until it becomes completely unmanageable.

How can Dagger 2 help?

Dagger 2 is an open source tool that can generate most of that initialization code automatically for us based on a few annotations. Using Dagger 2, the entry point of our application can be written with just a few lines of code, regardless of how many classes we have and how many dependencies they require. Below is the new main method for our example, using Dagger.

Note that we did not have to write a single line of code to instantiate any of the dependencies, or to specify how these dependencies are interconnected. This is done automatically for us. The class DaggerAppComponent, which is automatically generated during the compilation of the project, is clever enough to know, for example, that a WeatherReporter needs a Logger, a LocationManager and a WeatherService, which in turn need a GPSProvider and a WebSocket. With a single call to getWeatherReporter, it builds all these objects in the correct sequence, wires them together, and returns just what we asked for.

For Dagger to work, we need to perform a few steps. First, we need to add an annotation to the constructor of every class that Dagger needs to know about. Below, we show how this can be done for one specific class, but we also need to annotate every class that either requires some dependencies, or that is required by someone.

If you are worried about polluting your application with Dagger-specific code, then you will be pleased to know that the Inject annotation has been standardized (JSR 330) and works with many other tools, such as Spring and Guice. Switching to another dependency injection framework, then, can be done without modifying every class in the application.

For the next step, we need to create an interface, annotated as Component, containing methods that return the objects we need. It’s not necessary to create one method for each class in our project, but only for classes that are directly used by the entry point of the application. In our example, the main method only needs a WeatherReporter, so our component only needs one method, as shown below.

The only step left is to integrate Dagger with our build system. Using Gradle, this can be done by adding some new dependencies to the build.gradle file.

The project can already be compiled and executed. If you would like to try it yourself, the entire source code for this example is available at GitHub.

As a closing remark, note that every class in the project can still be used without Dagger, just as before. We can still, for example, instantiate a Logger manually using the new operator, perhaps during a unit test. Dagger does not modify the behavior of the language. It simply generates some convenience classes that know how to initialize objects for us.

In the next post of this series, we will see happens when the same dependency can be satisfied by multiple classes, or when we cannot annotate a class for some reason. If you have any questions, comments or suggestions, please let me know in the comments below.

Further reading

If you are not entirely convinced of the benefits of dependency injection, or would like to see more examples where it improves testability, please have a look at the the amazing guide Writing Testable Code, written by Miško Hevery, Russ Ruffer and Jonathan Wolter.

If you are more interested in the theory of dependency injection and all the variations out there, I recommend the essay Inversion of Control Containers and the Dependency Injection pattern, by Martin Fowler.