Let’s learn Dagger2 while refactoring a poorly coded Android app

Dagger2 and Dependency Injection are useful tools that can help us achieve a cleaner code, but learning them can be a struggle. I have a theory that Dagger2 by itself is very simple. What makes it difficult to learn is all the required knowledge about software organization and architecture. In other words, if you try to learn Dagger2 with a messy code, you’ll find it really hard. Try it on a “clean” code and you’ll be fine.

The main objective of this post is to highlight all the knowledge required to cleanly add Dagger2 to a project. However, instead of only talking about them, we’ll show with actual code!

We’ll refactor a poorly coded Android app. Its initial version has almost no “good practices”. Then, after a few refactoring steps we’ll reach a version using Dagger2 and, hopefully, a more clean and maintainable code.

This post is quite a journey. To keep it short, it’s more of a “show” than “tell” type of post. The important concepts will be briefly discussed, but it’s highly recommended to read more in-depth materials about them.

Our guinea pig

We’ll be refactoring Catter2; an app that shows pictures of cats wearing hats🐱‍💻. And users can save their favorite pictures.

The app consists of three screens, each one being an Activity:

  • LoginActivity: Used to enter and validate the user’s credentials.
  • FavoritesActivity: Shows the user’s favorite pictures.
  • ListActivity: Fetches and displays a list of pictures of cats wearing hats.

The code for the initial state of the app can be found in the following link: [our starting point]!

The tech

Refactor0: God Activities and the Single Responsibility Principle

If you have been learning Android development using materials from Google (*cough* Sunshine *cough*), then you have probably mastered the art of building God Activities; those Activities that does too much.

Take a look at the current version of our FavoritesActivity: [FavoritesActivity (initial version)].

Let’s list what this Activity is doing:

  • Loading user’s favorite pictures from disk
  • Processing user’s favorite pictures (model transformation to URLs)
  • Fetching the images
  • Displaying them
  • Navigation
  • Handling the behavior of button clicks

This Activity is doing too much and it can become unmaintainable rather quickly. That is an example of a God Activity. Also, there’s not much point in using Dagger2 in this Activity. So, before we proceed, let’s refactor our app with the Single Responsibility Principle in mind.

Finding purpose

Our first step towards a better app is to break our God Activities into smaller objects, where each object serves a single purpose.

To make this step easier, try to separate the app from the Android Framework. An Activity is not our app. An Activity is just a connection between your app and the Android Framework. The same goes for Android’s Services and Fragments. Try to view them as just a way for your app to exchange messages with the Android OS, and nothing more. Keep them as simple as possible and don’t put “domain logic” into them.

If you have any kind of logic that is specific for your app, like changing the ordering of images, then that logic shouldn’t be in the Activity. With that in mind, let’s list everything our app is doing:

  • Verifying user’s credentials
  • Loading user’s favorite pictures from disk
  • Adding a picture to the user’s favorite list
  • Processing the user’s favorite pictures
  • Listing pictures of cats wearing hats
  • Displaying pictures
  • Navigation
  • Handling behaviors of buttons

Once we have a list of tasks we can then start thinking about how we’ll organize them. The following list of classes is just one possible solution:

  • LoginService: Stores and verifies the user’s credentials. When an authentication succeeds it returns a token.
  • FavoritesRepository: Following the Repository Pattern, its purpose is to load and save the user’s list of cat pictures.
  • GetFavoritesUseCase: Transforms the user’s favorite pictures into a list of URLs ready to be displayed.
  • AddFavoriteUseCase: Adds an URL of an image into the user’s favorite list.
  • FetchCatImagesUseCase: Fetch cat images from TheCatApi.com.
  • LoginUseCase: Uses the LoginService to authenticate the user.

The complete refactoring step: [Refactor-0].

Refactor-0 walkthrough: Created the classes listed above. Everything related to favorites has been moved to a favorites package. UseCases will use Services. The Activities will delegate most of its logic to UseCases.

This is the most important refactoring step we’ll make! That change alone makes the code significantly more maintainable. The name of the classes clearly describes their purpose. Some code duplication was eliminated when we created the FavoritesRepository. But, more importantly, our Activities are much easier to read 😸

Note: The Activities are still doing too much. They are handling button behaviors, navigation and others things like animations. We’ll keep it that way for simplicity.

Architectural Layers

At this point we have four types of classes: Services, Repositories, UseCases and Activities. We can divide them into three layers: Infrastructure, Domain and Presentation.

The picture above shows how these layers interact with each other; note how the dependency only goes one way. Also, a layer depends only on the layer directly below it. Our ultimate goal is for the Presentation to know nothing about the Infrastructure layer; and the Domain layer should know nothing about the Presentation. When we accomplish this goal, testing and refactoring the system will be much easier. If you change something in the Infrastructure layer, then you only have to fix the Domain layer. Also Dagger2 will make more sense with this decoupling. But we are not there yet.

What does it means for our app? It means that UseCases will have no knowledge about Activities (dependency only goes one). Also, Activities will have no knowledge about Services (a layer depends only on the layer directly below it).

That paragraph was, probably, very hard to parse. Don’t fret! You can get better explanation on why and how we do it here: [Android-CleanArchitecture].

Refactor-0 recap: Identify and separate your code into classes where each one has a single purpose. Try to use an architecture that keeps the dependencies between the classes to a minimum.

Refactor1: Testing a UseCase

Let’s analyze the AddFavoriteUseCase from the previous step: [AddFavoriteUseCase].

The constructor takes a Context, an object from the Android Framework, and an userToken. And, the FavoritesRepository is being instantiated inside the UseCase’s constructor. Can we test this UseCase?

The most sensible answer would be “no!”. If we were to pass mocks of Context and userToken to the UseCase’s constructor, we would still also run code in our FavoritesRepository. In other words, a test on a UseCase is also testing the FavoritesRepository implementation. Therefore, it’s always a bad idea to test more than one layer at once. We should be mocking the Infrastructure layer in order to test the Domain layer.

What would the solution be? Well, that would be Dependency Injection (DI)!

Dependency Injection

Instead of letting the UseCase control the FavoritesRepository, we will only let the UseCase use it.

This step is very easy on our UseCases. Instead of letting them create their dependencies, we’ll just pass the dependencies to them–and that’s DI in a nutshell. See how simple this modification is: [AddFavoriteUseCase]. Now the UseCase is no longer responsible for the instantiation of the FavoritesRepository.

One interface, multiple implementations

FavoritesRepository is a concrete class, it can be instantiated. However, when we test our UseCase we don’t want to use its default implementation. So, instead of having a single implementation, we create an interface for it and then implement it. Here’s our new interface : [FavoritesRepository]. And here’s our [SharedPrefFavoritesRepository] implementation.

Now we can Stub the FavoritesRepository interface and pass it to our UseCase during testing: [AddFavoriteUseCaseTest].

See the diff for this entire refactoring step: [Refactor0->Refactor1].

Note: For the time being the burden of instantiating the FavoritesRepository has been moved to the Activity. It’s not the Activity’s job! We’ll fix it soon.

Tip: The UseCases doesn’t depend on the Android Framework. This means you can put them in a non-Android Gradle module, vastly improving building and testing times.

Refactor1 recap: Instead of creating the dependencies inside an UseCase, pass it as a parameter (this is DI). Use interface to indicate classes with multiples implementations. Easily mock/stub the UseCase’s dependencies. Don’t test more than one layer at once.

Refactor2: Instances and Lifetimes

Let’s complicate things by adding a cache to our requests to the cat API.

Note: The previous refactor included a TheCatAPI class: [TheCatApi(Refactor1)].

Adding cache is a simple task, see here: [CacheTheCatAPI].

The CacheTheCatAPI will store, in memory, the last response from the API. Subsequent requests asking for pictures will return the previous result. The FetchCatImagesUseCase remains exactly the same, isn’t it beautiful? 😻

But here’s a problem. The CacheTheCatAPI is instantiated during the creation of the ListActivity. In other words, this object is recreated on every configuration change (device rotation, resize, etc…), and, since the cache is stored in memory, it’ll be lost.

A solution is to simply extend the lifetime of the object; make the lifetime not dependent of the ListActivity‘s lifecycle.

A closer look at our lifetimes

This is a Sequence Diagram showing how we’ll organize the lifetime of our instances. TheCatAPI, with its cached version, is created when the app is created and will live forever (note the lack of an “X” at the end of its lifeline). Our UseCases will only survive as long as their associated Activity survives. And, the FavoritesRepository will be instantiated when the user logs in, and destroyed when the user logs off.

Accessing TheCatAPI from the ListActivity

We know we want TheCatAPI instance to survive a configuration change, but there’s no clean way to give the instance to the Activity.

An Activity can’t have a “normal” Java constructor where we just pass the instance like we did with the UseCases. We also can’t access the Activity’s instance after we create it with context.startActivity(intent).

A solution is to store the TheCatAPI‘s instance in a static variable. And, because its lifetime is the same of the application’s, the most appropriate place to store it is in the Application class: [Extending TheCatAPI lifetime].

Now the ListActivity can easily access the necessary instance, and the cache is now working as intended. Awesome!

FavoritesRepository lifetime

The same thing we did with TheCatAPI was done for the FavoritesRepository. However, the lifetime of this repository is only for when the user is logged in. See the diff: [User lifetime].

Note how the FavoritesRepository is also being stored in the App class, even though its lifetime is not the same as the App’s. We’ll fix it later.

Also note that we were passing a userToken between Activities before because it was needed to create the FavoritesRepository instance. We don’t need to pass the userToken anymore because we have access to the instance directly. One less “purpose” on our Activities, yay 😹

The full diff for this refactor is here: [Refactor2].

Refactor2 recap: Think about the lifetime of your instances; when they should be created and destroyed.

Refactor3: DI Components

When we moved the initialization of the TheCatAPI and the FavoritesRepository to our App class, we violated the Simple Responsibility Principle. The current App class is doing two things: initializing the instances of our services and distributing them to the rest of the system. Therefore, we need to break it into two types of classes: modules and components.

Also, the FavoritesRepository is only available when the user is logged in, so we should remove it from the Appclass and find a proper place for it.

DI Modules

A modules’ job is to create our instances. It has a set of “provide”-methods, each returning a different type of instance. See the [TheCatAPIDIModule] and its default implementation, [CachedRetrofitCatAPIDIModule].

Tip: The interface is optional, but can be useful if you have many different modules with different logic on how to initialize your instances. For example, one module will initialize your Firebase database, while another will initialize a SQLite database.

DI Components

Components’ responsibility is to hold the instances provided by the modules, and distribute them to the rest of the system. Each component will have its own lifetime, and all of its instances will share that same lifetime.

Let’s analyze the [AppDIComponent]; the component for the App’s lifetime.

It has a static instance, just like we were holding instances in the App class before. It has a initialize method that takes all the required modules as parameter. It has a public method distributing the TheCatAPI‘s instance. The AppDIComponent is initialized when the app is created: [App].

We also had another lifetime, the one that starts when the user logs in and closes when the user logs off. This lifetime is represented by the UserDIComponent: [UserDIComponent].

The UserDIComponent works similarly to the AppDIComponent . However, it depends on the AppDIComponent through the FavoritesRepoDIModule. See [FavoritesRepoDIModule].

Note: Components can depend on others components. Modules don't.

The UserDIComponent will “pass through” the AppDIComponent instances. This means that the rest of the system can access the UserDIComponent and get the TheCatAPI that is in the AppDIComponent.

As expected, the UserDIComponent is initialized when the user logs in: [LoginActivity(Refactor3)]. And it’s destroyed when the user logs off.

The Activities will then get the infrastructure instances from the UserDIComponent, see [Activity(Refactor3)].

The full refactoring step: [Refactor2->Refactor3].

Refactor3 recap: Modules create and provide the instances for the classes/types. Components take these instances and distribute them.

Refactor4: Testing Activities

With our current setup we could start testing our activities. See this commit: [FirstAttempt].

While this test works, it violates one thing talked previously, that is: “don’t test more than one layer”. You see, our Activity is in our Presentation layer and it’s mocking services from the Infrastructure layer. We should be mocking the UseCases from the Domain layer instead.

Injecting UseCase into our Activity

The same strategy we used to test our UseCases can be applied to our Activities. Don’t create UseCases inside the Activity, instead, pass them to the Activity. This way we can mock the UseCase and let the Activity use the mock!

But, as discussed earlier, we can’t have constructors in our Activity. To solve this, we’ll create an “inject method” that others can use to inject our dependencies. See [FavoritesActivity, inject].

From the previous Sequence Diagram, we know we have a lifetime for each Activity. So, we create both a Component and a Module for the FavoritesActivity‘s lifetime.

The FavoriteActivityDIComponent, see [Code], is a little different than the ones presented before. Since the component has the same lifetime than our Activity, we can safely store it in the Activity; no need for a static variable. Also, instead of the component having a set of “get” methods, it uses a inject method to inject the dependencies directly into the Activity.

The FavoriteActivityDIModule requires a UserDIComponent and can provide an instance of the GetFavoritesUseCase . The module uses a static variable to let our test class inject mocked UseCases.

The FavoritesActivity will use the Component to inject its dependencies, see [FavoritesActivity, inject].

Now the FavoritesActivityTest can test the Activity by mocking only the UseCases, see [Code].

Check the entire refactoring step, including tests for the ListActivity: [Refactor3->Refactor4].

Refactor4 recap: We can use DI with our Activity too, but instead of passing instances via the constructor, we pass them via methods.

Refactor5: Dagger2, finally!

We already have a system using DI! However, we can simplify things using a framework like Dagger2. If you understood all the steps we have done until this point, then you’ll find Dagger2 very easy to use.

Application lifetime

Let’s start migrating our system to Dagger2 with the AppDIComponent. The good news is that a Component in Dagger does the same thing our component is already doing. It holds the instances provided by the modules and distribute them to the rest of the system.

See how our Dagger2 version of the AppDIComponent looks like: [Code]. The annotation \@Singleton is used to indicate that this component is a singleton, or in other words, its lifetime is the lifetime of the App.

Dagger2 uses the Component annotation to mark an interface or an abstract class as a component. This annotation takes a list of modules as parameters.

As from our previous examples, we’ll store an instance of that component in a static variable and offer a initialize method. The initialization is slightly different because our AppDIComponent is now an abstract class. So we need to use Dagger2’s generated code to initialize it.

Tip: Dagger2 uses generated code to create the DaggerAppDIComponent class. If you’re getting errors, try building the project first.

Also note how our “get” methods are much simpler. All we have to do is declare their return type in an unimplemented method. Dagger2 will generate all the code for us :)

Our AppDIModule is a little different. Dagger2 requires a concrete class for modules. We can still override the methods later, so let’s just throw an exception if Dagger2 tries to use this class. See the [Code]. The TheCatAPIDIModule works the same way: [Code]. However, our implementation of these modules remains the same. See the CachedRetrofitCatApiDIModule: [Code].

Dagger2’s annotations are only needed in the base module class. You can omit them in the derived classes.

The initialization in the App class is exactly the same as before. See [Code].

User lifetime

Our new UserDIComponent is a lot more fun; check it here: [Code].

This component has a UserScope annotation ([Code]). This annotation is used to indicate the scope of our component.

Tip: If you’re unsure about scopes, try this link; or this one.

The \@Component annotation is different. It now takes a list of dependencies. This let Dagger2 knows that the modules inside the UserDIComponent can access the instances from the AppDIComponent. Because of that, the initialize method requires the AppDIComponent instance.

The FavoritesRepoDIModule (code here: [Code]) is very interesting. First, the provideFavoritesRepository now takes a set of parameters. They are used to create our FavoritesRepository‘s instance. Dagger2 will automatically find all the necessary parameters, including the ones from the AppDIComponent, and will pass them to this method.

The userToken provided with the provideUserToken method is only available for the UserDIComponent. No other component will be able to access it because UserDIComponent has no “get” methods for it.

The \@Named annotation is used for the userToken because its return type is a String, a very common type. Dagger2 will differentiate the “provide”-methods by their return type. If you have two methods with the same return type Dagger2 will get mad at you. To differentiate between the two, you can “name” them.

Tip: Don’t use the \@Named annotation, it’s easy to write the wrong name. Instead, you can create new types, like a class named UserToken. This is much easier with Kotlin’s data classes.

Activities lifetimes

Our Activities changed a little, see the FavoritesActivity code: [Code].

We no longer need the injectUseCase methods. Instead, we can just annotate our properties that we want to be injected with the \@Inject annotation.

The FavoritesActivityDIComponent (code: [Code]) changed a bit. It uses a custom scope, like the UserScope. And, instead of creating our inject method manually, we let Dagger2 inject those instances for us. Note how we build the DaggerFavoritesActivityDIComponent and immediately call inject with the Activity. This is because we don’t need to store its instance anywhere. All we care about are the UseCases.

The FavoritesActivityDIModule (code: [Code]) follows the same idea from the others modules, however, note the Lazy<FavoritesRepository> repositoryparameter. This tells Dagger2 to not try to instante the repository parameter until the repository.get() is called. By default, Dagger2 will create a new instance of the FavoritesRepository before calling the provideGetFavoritesUseCase. This is bad because we don’t need this repository when we have the testGetFavoritesUseCase. Therefore, the Lazy type helps us by preventing Dagger2 from instantiating the repository until it’s needed.

You can see the entire refactoring step here: [Refactor4 -> Refactor5].

And that’s it! We have our app using Dagger2. In my opinion, this last refactoring step is the most straightforward one. Once you have an architecture using DI and “good practices”, adding Dagger2 is a piece of cake🗡️🍰️

Can we improve?

The app improved drastically when compared to its initial version, but there’s still room for improvement. If you’re looking for some practice or want to share your improvements, then feel free to fork the project; oh, and don’t forget to let us know about it by opening an issue: https://github.com/AllanHasegawa/Catter2

Here’s a non-exhaustive list of things that can be improved:

LoginActivity

All of the “login” feature wasn’t properly refactored. Note how we don’t have a LoginDIComponent. Refactoring the LoginActivity using the techniques discussed in this post could be a good exercise.

Add new features/implementations

One cool thing about our architecture is that it makes it a lot easier for us to add new features and switch implementations. For example, we could use StorIO to save the user’s favorites. Or we could add a feature to remove images from the list.

[Advanced] Add a MV* framework

Our Activities are still doing too much work. One way to solve this is by using a MV* framework. You may need to create a new layer and rework the lifetimes.

Use Dagger2 in other ways

There’s no single, correct way of using Dagger2. This post showed one way of using it. Dagger2 experts out there, if you know how to improve this project, please, show us :)

Final words

Dagger2 is a very simple framework, but it requires a substantial amount of knowledge about code design to use it. Hopefully this post managed to show the most important techniques required to use Dagger2.

A quick recap of what we discussed:

  • Classes should have a single purpose.
  • The Android Framework (Activity, Fragments, etc…) is not your app. Keep it separated.
  • Organize your classes into layers; use only uni-directional dependencies.
  • Never use more than two layers in a single file/class.
  • Try to design testable classes; use DI when needed.

And, when designing your Dagger2 components/modules, try to:

  • Identify the purpose of your classes
  • Separate them if needed
  • Determine their lifetimes
  • How will we create them? (Module)
  • Who will have access to the instances? (Component)
  • Only then use Dagger2 to express your plan

Hope this post was useful 😺I skipped LudumDare for it 😿