I know there is already a ton of articles and tutorials about Dagger2 with most of them covering its usage and benefits. However, this article aims instead to provide an intuitive way to think about how Dagger2 wires dependencies together.
This article is also part of my personal learning and understanding; so if you find this useful or there is room to improve anything, please share your thoughts below!
Note: General knowledge about dependency injection is assumed. Although the actual name of the framework is Dagger2, it is commonly referred as simply dagger and that is what we will use in this article.
What makes up Dagger?
Dagger consist of mainly two encompassing containers — component and module. In this article, we will be focusing on how these two parts work with the framework and how dagger uses them to provide its functionality.
To start, let’s explore how we think about components and module.
Dagger component serves both as the overarching container that hosts modules and functions to allow functional classes to “connect” to the construction classes. The component we define with @Component will contain interface function that provide access to external classes that can then invoke them to request for dependencies. Component a set of modules to create those dependencies from.
We write components as interfaces, because the framework will generate the boiler-plate codes to wire all the modules together and provide injectors to the required objects.
You can think of component’s function similar to a chef de cuisine or head chef, putting together a set of dishes; and the component’s class as the kitchen.
Dagger modules on the other hand, contain implementation functions to create and provide relevant dependencies. This is why we write modules as abstract classes or objects (single static instance) and not interfaces. You can think of module as a chef de partie or cook, each being responsible for part of a dish.
Component and modules work hand in hand as you might expect in order to fulfill the requirements set out in the component interface and provide the needed objects into the classes that require them.
Let’s look at how they do all this in the context of the kitchen analogy.
Dagger in the kitchen
Let’s say in a restaurant, we only have one table to support, table 123 and we have an order to fulfill. We look at the Table123 class’s @Inject notation to find out what it needs.
Alright, seems like Table 123 is asking for a roast chicken. We need to fulfill this somehow. Back to the component, which is our kitchen, we need a method for any party outside of the kitchen to tell the kitchen to give it what it needs.
This would be what we need to define in the component interface; who to cook for & what is in the kitchen.
It might be more accurate to describe component’s interface functions as an access point to request for dependencies, by defining the function (usually named inject as convention) with the object type parameter, and what is available to be provided in this component( array of modules)
Now we need someone to actually cook the chicken.
Okay, since roast chicken is usually provided by a roast chef, it makes sense to group roast chicken under this chef’s responsibility. The roast chef can and will do the necessary and provide the roast chicken.
In case you’re wondering, the object, JvmStatic and singleton keywords are to indicate to dagger that we have only 1 RoastChef and 1 roast chicken so dagger will reuse them whenever we need a roast chicken. For the purpose of explanation let’s just assume the roast chicken will never be fully used up.
And that’s basically it! The roast chicken will magically be cooked and appear on table 123.
Just kidding, I know that is not satisfactory. We need to know how all these connects together and how it magically gets provided to the table!
How the kitchen functions
So what does dagger actually generate? For each of the @Provides annotated function, dagger will generate a factory class, whose job is to create and return a provider to the component.
The component will then create a local provider variable for each of the provide method and assign them values from the factory’s create function
Since Factory is a subtype of Provider, each Factory class can returns an instance of itself as Provider via the create function. If there is any dependencies needed to create the provider, the component will first pass them in the create function’s params.
Therefore, the component needs to and can decide what to provide, and when to provide dependencies to which party that needs it.
The end result is that we will have every provider in the component available in order to provide them to anyone who might need as the dependencies in one place.
You can think of it as getting all the chefs in one physical location so that the head chef can easily instruct them to provide what ever ingredient to make the dish any table may need at any point in time while having the ability to provide any ingredient the chef might need in order to prepare their respective dish.
Dagger then generates the implementation of the interface functions it needs to support like the cookFor(Table123) function we defined earlier on.
To supply the needed object or items table123 require, dagger creates a injectTable123 function to handle that. To understand that, we first take a closer look at how the whole component looks like.
If you look at the injectTable123 function, called by the cookFor function we defined, it knows table 123 require a roast chicken (from the @Inject notation) so it asks the injector to give it the roast chicken from the provider. You can think of it the Table123_MembersInjector as a server, who is responsible for putting things in the right place. The generated code for it is as below.
As referenced in the injectRoastChicken function, the server aka injector is given the table & the roast chicken and is just asked to give it to the required place.
A little more complexity
Now, to add an additional later of dependency to explore slightly further. What if the roast chef does not have the chicken? It means that the roast chef has to request it from the head chef.
In other words, if our module is defined as something like the following..
In this case, we’ll setup a module to provide the raw chicken.
Knowing how dagger generate codes now, we know that there will be a factory class generated for what we have defined in this @Provides function
The dagger component is now able to determine that a chicken is needed before a roast chef can provide a roast chicken and therefore creates a chicken provider and gives it to the roast chef. You can think of it as the head chef giving the roast chef the chicken supplier’s contact info so it knows where to get it.
To illustrate further on how dagger fulfill this added dependency, we can see that the provideRoastChicken function now gets supplied with the chicken from the chicken provider whenever roast chef needs to provide a roast chicken.
To sum it all up
So to provide a recap and sum up all that we have discussed, here’s a diagram to illustrate all the structure of our kitchen example versus dagger’s generated code
I hope this article gave you a little fun intuitive way to think about them and shed some light into what dagger actually does when you use the framework.