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
- The user’s favorite pictures are saved using Android’s SharedPreferences.
- The cat pics are from TheCatApi.com service. Catter2 uses Retrofit to interact with the service.
- TheCatApi.com sends a list of URLs. The images are downloaded and managed with Picasso.
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 theLoginService
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 App
class 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> repository
parameter. 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 😿