Dagger dependencies beyond the basics

Fabio Collini
ProAndroidDev
Published in
7 min readApr 8, 2020

--

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 Providers 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 as object
  • if the class is referenced using an interface: add a Binds annotated method in a module defined as interface
  • 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 a Provider

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.

Sign up to discover human stories that deepen your understanding of the world.

--

--

Published in ProAndroidDev

The latest posts from Android Professionals and Google Developer Experts.

Written by Fabio Collini

Android GDE || Android engineer @nytimes || author @androidavanzato || blogger @codingjam

Responses (2)

What are your thoughts?