Taking a stab at Dagger 2

Abstract

This article takes a look at Dependency Injection as a whole, what Dagger 2 is, and how we can implement DI through a very simple Android example.

Dependency Injection

Quoting the Wikipedia page for Dependency Injection, we get that:

In software engineering, dependency injection is a technique whereby one object supplies the dependencies of another object.

Dependency “injection” of sorts

In the first line, we have a regular instantiation the present context passes all of the required information to some class, called User, to create a new instance. We get back John Smith, aged 34. The second two examples show a sort of dependency injection — where some method provides the calling context with the User instance that it needs. The key difference here is that the present context (whether it’s an Activity, a Fragment, or some custom view), doesn’t know anything about how to instantiate that User. All it knows is that it needs a user, and one of those two approaches will give it said User.

This is dependency injection in a rather succinct nutshell. It allows for the separation of concerns — the thing that needs class X does not need to know how to make class X, that’s the job of DI.

Let’s look at another example. Say your application needs to execute network calls. A new instance of your NetworkClient class requires an API key during instantiation. What’s the problem of just passing in the key every time you need this? From every Activity, Fragment, View, wherever? There are several problems:

  1. You’re letting everything know you’re getting your data from the network, and that your application has an API key.
  2. You’re typing the same instantiation call over and over again to get the same thing. OK, you can use a singleton design pattern, but why not have DI provide you with the instance everywhere you need? Write once, access everywhere (patent pending)
  3. What if for some reason, you need to pass this instance of NetworkClient between two classes? You need to write custom serialization and deserialization logic to turn NetworkClient from an intent argument back into itself.
  4. Unit tests — testing something that now relies on NetworkClient means you have to mock the latter. What if you change something in the network logic? Well, now you have to mock out the change, there go the unit tests…

I brought up a simple example where we only need an API key, imagine these 4 points, but with a class whose definition is this:

An imaginary DeviceController class

As much as I love writing unit tests, I do not want to mock out every single dependency of that class just to make sure it…opens a connection properly? Parses the response correctly? Whatever it may be doing.

Wouldn’t it be nice if you could just ask for a DeviceController and get it? Without having to create a UserManager, Executor, ConnectionManager and so on? Well, you can, with DI! That’s what it’s designed to do — give the calling context everything it needs to do its job, without actually exposing how something was instantiated. It also helps with unit tests — one can inject dependencies instead of mocking everything under the sun.

Dagger 2

Dagger 2 is a dependency injection framework for Android, created by Google. It’s a follow-up to Dagger 1, which was initially made by Square. Dagger 2 allows a developer to inject dependencies into Activity, Fragments, custom Views, your services, etc. Whatever you create in your Android application can be specified as a “destination” for Dagger to inject dependencies into.

One can also isolate dependencies from one another. If one Activity needs class A, but not class B — Dagger is configurable to allow this. Check out the GitHub page here, which will also tell you how to include it in your Android project. We’re going to be using Kotlin for this example. You don’t need to do anything special for your Android Studio project, a blank activity is fine. 😃

A simple Injection

Bare Example

Let’s inject something very simple — an AndroidLogger class, which will have just one function. We are not going to use anything fancy for this example. Let’s look at the implementation:

Our AndroidLogger class

How do we do this? We have to add an annotation to this class. Here, we need to add @Inject constructor() to the class definition. Why? Well, first, @Inject tells Dagger that the constructor is what it should call to create an instance of AndroidLogger. Since we are using Kotlin, we have to specify the no-arguments constructor explicitly. Our class now becomes:

AndroidLogger ready to be injected

The second step is to create a Dagger component. A component is usually an interface which contains a list of “destinations” (Activities, Fragments, etc.) where Dagger can inject what it knows. What does this look like? Well, we add @Component to the interface, and pass our destinations into the injection methods.

The component for AndroidLogger

Some things to mention:

  1. Dagger does not care what you name injection methods in the component. You can call it awesomeMethod, and it will still work. What matters is the parameter passed in. That’s what tells Dagger “Hey, you can inject your dependencies into MainActivity”. Using inject as the method name is a convention found in many Dagger 2 projects.
  2. Point 1 applies to the interface name itself. It can be called anything, as long as it’s annotated with @Component. There’s another convention in play here, which will be explained in the next part.

We’re almost done! Rebuild your project and go into MainActivity (or whatever destination you specified for Dagger to inject into). Let’s start with the simpler part — adding your variable to MainActivity. Since the variable is injected by Dagger, you have to give it the @Inject annotation, and the lateinit property. The latter is required because…well, the variable hasn’t been initialized yet. It’s initialized by Dagger during injection. Add this as a member variable:

@Inject lateinit var logger: AndroidLogger

The more complicated part comes next. When you rebuilt the project, Dagger generated code required to work with your component. As a result, you should be able to autocomplete DaggerAndroidLoggerComponent (it will be Dagger<YourComponentName>). In your onCreate function, right before the super() call, add the following line:

DaggerAndroidLoggerComponent.create().inject(this)

What did we do?

  1. Told Dagger to create the component DaggerAndroidLoggerComponent by calling the generated method. If you jump to the definition of create(), you’ll see that it’s invoking a builder. An equivalent statement is:
DaggerAndroidLoggerComponent.builder().build().inject(this)

2. The builder pattern will return an instance of whatever it’s supposed to build — so DaggerAndroidLoggerComponent. After creating the component, we tell Dagger to inject it into MainActivity. This is where the name of the injection method comes into play. If you called the method awesomeMethod, then you will need to call awesomeMethod(this).

You’re done! Full activity is shown below:

MainActivity with dependency injection

Some organization

Let’s expand on the above example. Let’s say that we have a Summary class, which requires AndroidLogger to log things. Here’s an example implementation:

Summary class, which requires AndroidLogger

Summary needs AndroidLogger to print out the length of the passed in string. Let’s add this new function to AndroidLogger:

fun log(parentTag: String, message: String) {
Log.i(TAG, "Incoming message from $parentTag")
Log.i(parentTag, message)
}

Cool, so how do we inject Summary, which also relies on AndroidLogger? Let’s use a module! In Dagger, a module is class, annotated with @Module that is used to organize (group) related dependencies. For example, if you have multiple network clients, you may want to organize all of them in a NetworkModule. This is a naming convention used for Dagger — modules are named in the form of <Purpose>Module. Since we are logging things, let’s call this our LoggingModule.

@Module
class LoggingModule {
//Stuff will go here!
}

This looks a little bit bare — it currently does a whopping amount of jack-all. How do we tell it that it provides certain dependencies? Well, we write functions in it, annotated with @Provides, which actually create the required dependencies! What does this mean? Well, let’s look at AndroidLogger — it doesn’t need anything, so let’s write a @Provides function to instantiate it:

@Provides
fun provideAndroidLogger(): AndroidLogger {
return AndroidLogger()
}

Several things to explain about what we did here:

  1. Annotating a method with @Provides tells Dagger “Hey, when you need an instance of X, call this method”. This is similar to adding @Inject constructor() to the class header.
  2. The return type of the method is what matters here — you’ll see these methods named like providesX or provideX for a method which instantiates X. Another naming convention!

Now, let’s write the exact same method, but for our Summary class. You may be wondering: “Well, how do I get AndroidLogger in there?”. The answer is simple — you just pass it into the providing method. The reason this works is because of Dagger — it knows how to give you an AndroidLogger because of the method above. Behind the scenes, Dagger generates a dependency tree which allows it to see how the classes are interdependent on one another, as well as which classes it is able to provide you with. Here’s the complete LoggingModule:

Our completed LoggingModule

The hardest part is done! Let’s wrap this up — we have just a couple more one one-lined changes to make. First of all, remove @Inject constructor() from AndroidLogger. This is because now, instead of directly calling the no-arguments constructor for AndroidLogger, Dagger will be using the @Provides method.

Second, because we used modules to organize our dependencies, we need to link the module up to the AndroidLoggerComponent interface. This is done like so (adjust the interface’s annotation):

@Component(modules = [LoggingModule::class])

Dagger allows you to wire up multiple modules to a single component, hence the array syntax. We are done! Now, if you want to use Summary in MainActivity, simply create the injected lateinit variable and use it. Final MainActivity is shown below:

Dependency injection at work

You should see something like this in the logs:

2019–08–12 00:57:46.011 3334–3334/com.demo.dagger I/AndroidLogger: Hello, world!
2019–08–12 00:57:46.011 3334–3334/com.demo.dagger I/AndroidLogger: Incoming message from Summary
2019–08–12 00:57:46.011 3334–3334/com.demo.dagger I/Summary: The summarized length is: 17

@Singleton

Although the above example will work well, there’s a bit of a kind of sort of glaring issue. Every time you’ll need Summary or AndroidLogger, you will get a new instance of it. How do we prevent this? We add the @Singleton annotation to each provider method, as well as to AndroidLoggerComponent. This way, Dagger will return the same instance whenever you need AndroidLogger or Summary. The interesting thing here is that a Dagger singleton is not the same as a regular singleton, it’s a scope. Some pretty good answers on that are here.

Conclusion

Hopefully this has shed some light on Dagger and how to quickly get started with DI on Android. If you’re one of the two people reading this (Hi, mom, hi, dad!), and liked this article — leave a clap, comment, share. Feedback and questions are always appreciated in the comments! 🚀

Android Software Engineer @ Google. CS and Nuclear Engineering by education, one is just…way less explosive.