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
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 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.
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
- 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.
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
- 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
LoginServiceto 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.
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
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)!
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
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.
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].
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
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
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].
ListActivity can easily access the necessary instance, and the cache is now working as intended. Awesome!
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.
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.
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].
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.
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 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.
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
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
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.
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.
FavoritesActivity will use the Component to inject its dependencies, see [FavoritesActivity, inject].
FavoritesActivityTest can test the Activity by mocking only the UseCases, see [Code].
Check the entire refactoring step, including tests for the
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.
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 :)
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
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].
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.
\@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
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.
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.
\@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.
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
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.
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:
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 :)
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 😿