Improving testability in Android MVVM with Dagger 2
When adding new dependencies to projects it is essential to evaluate the actual benefit that is provided and also if complexity is heavily increased, ultimately weighing these two factors.
As you might know, Dependency Injection (DI) is a key concept that allows your code base to be highly testable.
Following this idea, the easiest way to achieve DI in Android today is to integrate Dagger 2, a DI framework which uses code generation and is based on annotations so that developers no longer need to handle dependency injection manually — a task that becomes extremely laborious in extensive projects. We will not get into details on how Dagger works and why to choose it, as is not the scope of this article — check the footer for extensive reading.
Now, every well-informed developer should consider structuring apps through presentation patterns, this way enforcing decoupling throughout the project. One of patterns on rise lately is MVVM (Model-View-ViewModel) and from obvious reasons, everybody should try giving it a go. One of the main reasons when opting for MVVM is the clear separation between Views and their master, which is ViewModel — this actually doesn’t happen in MVP where masters (this time Presenters) hold references to Views. I am not going to enumerate more reasons nor go into details on why to opt for MVVM as you can check the footer for extensive reading on this topic.
Note: using MVVM and Dagger 2 results in a highly scalable and testable app, therefore you should consider that structuring your app with MVVM and Dagger is required for complex projects.
The only question that remains is how to blend these two key features into your project?
The best way to understand these concepts is to learn by example. Let’s then setup from scratch a MVVM app and integrate Dagger into it. We will be using Dagger’s Android specific helpers that are provided by Dagger-Android. Their main advantage is that providing Application and Activities/Fragments into the dependency graph is now much easier and cleaner.
The sample is available on GitHub and I highly recommend you to clone it and examine it while reading this article as some minor classes are not shown below.
Project overview:
Package data
represents the Data Layer, while di
gathers all Dagger-related classes.
The ui
package contains all the screens of the app and message
is basically our only screen in this sample. It contains both the classes representing the View (which are MessageActivity
and MessageFragment
) and the ViewModel layer contained by MessageViewModel
.
We can also notice other less-important packages like scopes
and utils
in which we won’t get into details.
Now, before we get our hands dirty, let’s get a quick overview on how the dependency graph will look like through the below diagram:
Next, let’s dive right in and set up the project by adding these dependencies to build.gradle(Module: app)
file:
For the sake of simplicity, our only screen (representing the View in MVVM), will display a message through a fragment.
Let’s start off by defining a class that represents the application, and let’s call it simple Application.java
. This class extends DaggerApplication
in order to let the application inject its members and therefore to be used in injecting Activities, Fragments, Services etc. that are attached to it.
Note: at this point, the IDE won’t recognize the symbol DaggerAppComponent
— don’t worry, at the end when we will rebuild the project, the AppComponent
will be generated and the error will disappear.
A very important step now is to add this Application class to our manifest, so that Dagger can recognize it.
<application
...
android:name=".Application"
</application>
Now, we create a di
package that will contain all essential Dependency-Injection related classes. Let’s first define the app’s Dagger module by creating a AppModule.java
class. Basically, this class is used in order to bind our Applications class as a Context in the AppComponent
(which we will define immediately). Using Dagger-Android, we no longer need to pass our Application instance to any module, we are now simply exposing the Application as Context.
Now let’s define Dagger’s AppComponent
which has these modules: AppModule
(that exposes application), ActivityBindingModule
(in which we will be defining which activities to be injected), AndroidSupportInjectionModule
(which is the module from Dagger-Android that helps with the generation and location of subcomponents — activities in our case) and finally ViewModelModule
(in which we will define how ViewModels will be provided). Now, in this high level component, we have an inner interface called Builder
which allows us to customize the builder for components.(More extensive read can be found in this amazing article).
Wait but what is @AppScoped
? Well, it is just a custom scope that acts exactly like @Singleton
, allowing components scoped with this annotation to be instantiated only once amongst the app. Why @AppScoped
and not @Singleton
? I strongly believe that naming this annotation AppScoped is much more suggestive and improves readability. If you browse through scopes
package, you will notice that there are other lower-scope annotations that will be used across the project.
Now, we have to define ActivityBindingModule.java
which allows Dagger-Android to create a subcomponents for each screen (activity) automatically. As ActivityBindingModule
is directly attached to AppComponent
, its parent component is therefore AppComponent
. As we will only have one screen (activity):
Now we notice that we have to define our activity and its module.
Let’s leave the MessageModule
for later, and let’s first add the activity MessageActivity.java
, its layout file activity_message.xml
, a blank fragment called MessageFragment
and its layout file fragment_message.xml
in a package that defines the screen called message
within a ui
package that gathers the app’s screens.
Note: Some prefer to name both Activities and Fragments as views (which is also correct), yet in this case we will refer to a View as duo represented by an Activity
and its child, the Fragment
.
Let’s start with the Activity. To assure that MessageActivity.java
is available for DI with Dagger, it extends DaggerAppCompatActivity
. We are also injecting our MessageFragment
— and as for the Activity’s role, it is quite simple in this scenario, the activity should just launch the fragment:
Before adding the fragment, let’s quickly define an interface for our Views in MVVM, this way we allow future Views to bind to the ViewModel under the same terms, enhancing scalability and maintainability.
Now let’s define our MessageFragment
which in MVVM ecosystem is a View that inherits BaseView
, therefore has BindViewModel
and UnbindViewModel
methods - this way, the ViewModel has no reference to the View, so the View simply binds to the ViewModel, therefore allowing multiple subscriptions to the ViewModel.
A very important difference from MVP is that the ViewModel here cannot be directly injected, and therefore we have to create our own factory (which we will do in short time) that provides it and then inject it. From that factory we can then get a reference to our ViewModel.
Also, we can notice how data is processed through UI models, which is MessageUiModel
in our case, a model that simply denotes a string and whose definition we will implement in short time.
Note: @ActivityScoped
is an annotation just like @AppScoped
, yet now it forces all components annotated this way to live as long as the Activity does, — in our case, we want MessageFragment
to live as long as MessageActivity
does.
In order to allow Dagger to inject our MessageFragment
in MessageActivity
we first must add an @Inject
annotated constructor, as you can see above. Then, we must define MessageModule
, module which tells Dagger exactly how to provide the fragment as this module is part of ActivityBindingModule
. Here, @FragmentScoped
annotation is used to specifically denote that Fragments
are lower-level than Activities
.
Now, at this point Dagger should be able to provide us MessageFragment
within MessageActivity
. But Dagger still can’t provide us the ViewModel as we haven’t defined neither MessageViewModel
nor the factory that provides it, named MessageViewModelFactory
.
Now, ViewModels have different scope from Presenters in MVP, they should now live as long as the application does, hence they are annotated with @AppScoped
— this way, Views can attach/detach to the same instance of the ViewModel when configuration changes occur, this way any data stored in VM is persisted.
Let’s see how a dumb ViewModel would look like:
Note: In this article, for the sake of simplicity, MessageViewModel
has only necessary methods, yet it real-life scenarios, it should be implemented in a much extensive way (adding more logic to it, etc).
Now that we stumbled upon MessageUiModel
again, let’s get a quick look at how this mysterious piece of code looks like:
What are UI Models? They are basically an abstraction for what content is delivered to the Views. For example, if we had a list of objects, we would just add a List<Object>
field into the UI model.
Now let’s get back to serious stuff — let’s provide the factory class for our ViewModel, adding it inside the di
package.
You would think that finally Dagger knows how to provide the ViewModel for us, but think again! We forgot to define the ViewModelModule
! Basically, this module tells Dagger how the factory is provided and then the MessageViewModel
.
Note: This module should contain all providers for all your future ViewModels, hence the abstract name ViewModelModule
— therefore when adding a new ViewModel to your app, don’t forget to let Dagger know about this by adding it into the ViewModelModule
as well.
Phew, this is pretty tiring! But we are almost there, all we have to do is define a fake repository that provides us some data.
If you remember, we injected the repository inside the ViewModel, so we need to tell Dagger how to provide it by adding an annotated constructor.
Note: In real-life scenarios, the repository should be more complex and it should get the data from different sources and it would also implement the same interface as those sources.
We’re finally done!
If we rebuild the project now, all red lines should go away and the app should display through our View the message Hello Medium! .
As the app’s skeleton has been built, at this point, adding Views (Activities & Fragments) as well as ViewModels is a piece of cake.
Important note: you should consider integrating RxJava2 into this project if you plan to use this boilerplate for production.
Further reading resources:
That’s it! Thank you for using your precious time to read this article.
Note: Following this project, another article will come soon describing how easy is testing such setup.