
Dagger dependencies beyond the basics
Dagger is one of the most polarizing subjects in the Android community, either you love it, or you hate it. Some developers love it and say that it reduces boilerplate code. Other developers have different opinions and say that it’s too complicated and really verbose. So, does it reduce the boilerplate or is it too verbose? As it often happens in software development, the answer is: it depends!
The two main concepts in Dagger are components and modules. So the answer to the previous question is it depends because defining components involves a bit of boilerplate code but writing modules (or avoid defining them) allows reducing the extra code to write.
The goal of this post is to analyze how to define dependencies in a way that reduces boilerplate code. Defining dependencies is a basic concept in Dagger so there is already a lot of material about it. However, it’s worth discussing in a new post because there are some new Dagger features that allow simplifying the configuration needed to declare dependencies (especially if the code is written in Kotlin).
Inject annotation
Let’s start with an easy example, an UseCase
class with a two arguments constructor:
Thanks to the Inject
annotated constructor Dagger knows how to create a new instance of the UseCase
class when it’s necessary. This is the easiest way to manage instances of a class using Dagger, unfortunately there are some limitations. It can be used when:
- a class can be instantiated directly using the constructor
- the creation is not managed by an external framework (for example activities and fragments)
- the source code can be modified to add the annotation
In a perfect (probably utopian) world boilerplate code is minimal when all the classes can be managed in this way. Here’s another example of a “complete” minimal software:
Apart from the Inject
on the constructors, there are just four extra lines of code needed to define the Component
. Adding an extra dependency to a class is trivial: just add the Inject
annotation to the dependency class constructor (if it’s not already present) and add a parameter to the dependent class constructor:
A real project is usually not so simple, there are some extra cases that need to be considered. So let’s see how to manage real stuff.
Dependencies managed using interfaces
Let’s go back to the UseCase
example, the first constructor parameter is of type Cache
. This is an interface, the concrete implementation is DatabaseCache
:
How can we manage Cache
’s instances using Dagger? A method in a Dagger module seems to be the obvious solution:
This solution works but we are not leveraging Dagger because both the DatabaseCache
class and the provideCache
method need to be changed in case of a new dependency.
There is a better solution, checking the DatabaseCache
class it ticks all the requirements in the previous paragraph. The Inject
annotation can be added to the constructor:
Dagger knows how to create DatabaseCache
instances but it’s not aware that a DatabaseCache
can be created when a Cache
is needed. Let’s fix it using a module method:
A new dependency can be added to DatabaseCache
changing just the constructor arguments so that the provideCache
method doesn’t need to be modified.
This solution can be improved further, checking the generated code we can see that there are two factories: the first one for DatabaseCache
(generated based on the Inject
annotated constructor) and the second one for Cache
(generated based on the Provide
annotated method). A better solution uses just a single factory to create instances of both classes which can be achieved by using the Binds
annotation:
A Binds
annotated method contains a parameter with the implementation and declares the return type as the interface, the body is not necessary so it must be defined abstract
. For this reason, a class that defines a Binds
annotated method must be defined as abstract
. Here an interface is used to avoid using the abstract
keyword and to keep the code even more readable (eight fewer characters in the code!).
Provide annotated methods in a module
The Inject
annotation on the constructor cannot be used when an object must be created with a factory, builder, or defined in an external library. A perfect example is a Retrofit service. The implementation is created using a Retrofit builder so the constructor of the concrete class cannot be annotated. In this case a Provides
annotated method in a module can be used:
It’s worth noticing that this module is defined as anobject
instead as a class
, this little change allows Dagger to generate less code. Indeed the provideApi
method can be invoked without creating an instance, for a similar reason a field with the module instance in the factory is not generated.
This example uses two modules: MyModule
defined as an interface and MyApiModule
defined as an object. The Dagger component must depend on both. An alternative solution that allows defining a single module in the component dependencies is the following one:
The method previously defined in MyApiModule
is defined in the companion object of MyModule
. Using this syntax only MyModule
must be referenced by the component that will use the methods defined in the companion object as well.
Interfaces with multiple implementations
The Binds
annotation can not be used in case there are multiple implementations for an interface. For example, if there are two Cache
implementations (one that uses a database and one that uses an in memory cache), a Provides
annotated method in a module can be used:
Even though this method works as expected, it can generate a big issue: both the implementations are instantiated, no matter which we will use. We can solve it by using yet another Dagger feature, using two Provider
s allows deciding the instance to create:
Provider
is an interface with a single method get.
It’s useful when we want to create multiple instances of a class or when, as in this example, an instance is created only based on some conditions. Here we could have used theLazy
class as well, the difference is that Lazy
always returns the same instance, instead Provider
creates a new instance every time the method is invoked. Both Lazy
and Provider
objects can be injected using Dagger, they can be defined as arguments in Provides
annotated method or in an Inject
annotated class (both as field or constructor arguments).
Classes managed by a framework
Sometimes (often in the Android world) a class is created by a framework using the default constructor with no arguments. In these cases the Inject
annotation must be used on fields (that must be defined as lateinit var
to allow Dagger to fill them with an instance after the creation):
The inject
method defined in a Dagger component allows populating all the Inject
annotated fields. There are a lot of concepts behind this single method, however they are out of scope for this post.
A ViewModel
is something in the middle, the lifecycle is managed by the Android framework but it can be created using Dagger like a “normal” class. A common solution uses Dagger multibindings, it works but it’s a bit complicated and not completely compile time safe. However there is an alternative way to manage a ViewModel
using Dagger: a Kotlin delegate. I already wrote a post about a similar subject some times ago but now the solution it’s even simpler.
First of all using Dagger, a Provider
that creates the ViewModel
can be obtained easily: it allows creating the real instance only when necessary. Then the activity-ktx library contains a viewModels delegate to simplify the code necessary to manage a ViewModel
. Can we leverage these two concepts to create a new delegate that creates a ViewModel
starting from a Dagger Provider
? Of course we can, here’s the code:
To be fair this code is not easy to read (it contains all the most complicated Kotlin keywords!), the good news is that it can be defined once and used in all the activities (something similar can be created for fragments). Here’s an example of usage (the MyViewModel
is declared in the usual way using an Inject
annotated constructor):
The code is simple, there is no need to use multibindings and we get a compile time error in case the ViewModel
Dagger configuration is missing or wrong.
Wrapping up
A Dagger dependency can be defined in many ways, here there is the logic that can be used to decide the best way based on the type of class:
- when the class can be created in the code: add the
Inject
annotation to the constructor - if the class is defined in a library or instances are created using a builder or a factory: add a
Provide
annotated method in a module defined asobject
- if the class is referenced using an interface: add a
Binds
annotated method in a module defined asinterface
- activities, fragments and other classes managed by the framework: add the
Inject
annotation to the fields and populate them invoking a component method - viewModels: define them as a normal class adding the
Inject
annotation to the constructor and then create the instance in the delegate using aProvider
Following these steps the boilerplate code necessary to define Dagger dependencies can be minimized. The project used in the examples of this post can be found in this repo.