After developing a cross-platform mobile application in Xamarin, working with the MvvmCross framework to increase the amount of shared code between platforms, I wanted to bring MVVM to native Android and reap the benefits of a cleaner, more loosely coupled architecture. We've been using this architecture when developing Android applications ever since, to great success.
The official Android documentation does not provide much guidance at all on application architecture, it is fairly open ended and un-opinionated, leaving it up to the developer to structure their application. After coming across and going through the code of many Android apps from other developers, I'm publishing my thoughts on the MVVM approach to give developers guidance on application architecture, so that developers can learn how to build scalable, extensible, and maintainable apps. The main problems that I commonly see are humongous activities and fragments, filled with non-view related code - causing them to be extremely brittle and unmaintainable. The MVVM architecture helps to alleviate this problem.
What is MVVM?
MVVM is a software architecture design pattern, which facilitates the separation of the user interface from the business logic and data model of the application. It is prevalent in .NET technologies such as C#, as this is where it was originally conceived. MVVM stands for Model View ViewModel, which describes the three different components of the architecture.
Model
The model layer consists of the application's domain model, which can include data access (repositories, DAOs, APIs), entity classes, and validation rules. In Android, you will typically see entity classes as POJOs, or that extend and/or use annotations from the chosen ORM (greenDao, Realm, OrmLite).
The model does not know about either the viewmodel or view, it exists independently. If porting an already developed Android application that is built upon the MVC pattern, the model layer will most likely not need to change. The model may broadcast that changes have been made, which can be subscribed to by the viewmodel, and subsequently passed onto the view.
View
The view is what the user sees. In Android development, views are typically defined in XML, and further customised in the activity, fragment or view classes. In a lot of Android applications, you will often see a lot of non-view related code, especially business logic, mixed into fragments and activities - which can grow and become very difficult to extend or maintain. MVVM combats this problem, by keeping the activities and fragments purely as part of the view layer - they should only be responsible for setting up the views, as well as binding the viewmodel to the view (with tools such as RxJava).
Unlike in MVC, the view does not know about the model. It is only aware of the viewmodel, from which it gets its data to display. It should be free from display logic (date formatting, rules to show/hide an element, and so on), which will be present in the viewmodel instead. The view can request the viewmodel to perform operations, which may update the model, usually based on user input.
Viewmodel
As implied by the name, the viewmodel is the bridge between the view and the model. It draws its data from the model layer and transforms it for displaying. The viewmodel holds the active state of the application. It exposes operations to the view, providing an interface for updating the model. Viewmodels are POJOs, which means that they are easily testable - one of their main advantages.
Comparison to MVC
MVVM shares several concepts with the more commonly known MVC (Model View Controller) pattern, so those that are already familiar with MVC should be able to pick up MVVM quite easily. The model and view are largely the same in MVVM as they are in MVC, what is different is the way in which they communicate, as well as their awareness of each other.
Benefits of MVVM
Viewmodels allow easier testing of display logic
Viewmodels allow the display logic to be tested without ever instantiating the view. In Android development, this allows more tests to be written in JUnit and run locally, rather than on the Android JVM (simulator or device) - meaning that tests are much quicker to run. Viewmodel dependencies can easily be mocked using tools such as Mockito.
Separation of concerns
Tightly coupled and brittle code makes for a maintenance nightmare. Bugs can easily be introduced when trying to extend such codebases, new features can take much longer than expected to implement, resulting in poor customer satisfaction. One of the main culprits of this that I often see in Android applications is monolithic activities and fragments - containing business logic, interacting directly with the database and/or API, mixed in with all of the view code. MVVM helps greatly to keep architecture clean, cohesive, and loosely coupled - keeping activities and fragments purely as part of the view.
Improves code reuse
Viewmodels can be re-used throughout the application, meaning that display logic does not need to be duplicated. The view can even be swapped out for another and still use the same viewmodel - as long is it depends on the same data and operations.
User interface can be updated without touching the business logic
This means that components can be worked on independently by different developers with fewer conflicts, allowing the team size to scale more easily. For example, a front-end developer is able to independently work on the view, without needing to touch logic in the viewmodel, which can be worked on by another developer. A mock version of a viewmodel can be used while the real one is being developed, or to avoid having to rely on an API during development - the API may not even exist yet, or is being actively developed separately.
MVVM example - Reddit reader
Now that we know what MVVM is, as well as its benefits, it is best to develop an Android application using the MVVM architecture to demonstrate how it is put into practice. We will look at building a simple Reddit reader, for which the project can be found on our GitHub.
The application
The application is very simple - it pulls down the top posts on Reddit from its JSON API and displays them in a list. It uses infinite scrolling for loading more posts, and shows a loading indicator while they are being loaded. We may look at extending the application in further posts, showing further usage of MVVM or focusing on other topics - building up to a full production-ready app.
The main libraries used in the project are:
- RxJava - mainly for data binding
- Retrofit - Reddit API calls
- Dagger 2 - dependency injection
- Picasso - image loading
- AndroidAnnotations - reducing boilerplate code
- Lombok - eliminating getter/setter boilerplate
A prior understanding of reactive programming will be useful for following along, as the examples quite heavily use RxJava.
To keep this post from becoming too long, only the essential code snippets for demonstrating MVVM will be shown, while others will only be linked.
Building the model layer
To get the top posts on Reddit, we can use the http://reddit.com/top.json endpoint with no authentication. An example, unminified, response can be seen here.
The response is made up of several nodes, each with a 'kind' field that describes the type. An object can be a listing, link, comment, etc.
The top posts response is firstly made up of a 'listing' parent element, that contains a list of children (which are links), as well as before and after tokens for pagination. The model is very simple, and looks like the following:
public class RedditListing implements RedditObject { private List<RedditObject> children; private String before; private String after; }View on GitHub
RedditObject
( view on GitHub) is currently an interface that all types extend from.
The model for the children of the RedditListing
, RedditLink
looks like the following:
public class RedditLink implements RedditObject { private String id; private String title; private String domain; private String subreddit; private String subredditId; private String linkFlairText; private String author; private String thumbnail; private String permalink; private String url; private int score; private int ups; private int downs; private int numComments; private boolean over18; private boolean hideScore; private Date created; private Date createdUtc; }View on GitHub
Not all fields from the response have been included, as they are not currently needed.
Retrofit is used for making the Reddit API calls. The client interface is as follows:
public interface RedditClient { @GET("/top.json") Observable<RedditObject> getTop(@Query("after") String after, @Query("limit") int limit); }View on GitHub
The after
query parameter is used for pagination, where a previously returned page token can be used to get the next page of results after that response. The limit
query parameter can be used to limit the number of results returned.
Gson is used to convert the JSON responses into the model classes. As the types in Reddit responses are dynamic, defined by the 'kind' field, a custom deserialiser was written to deserialise objects into their concrete types ( view on GitHub). This is why the interface RedditObject
is used for all models, even though there are no common fields.
Building the viewmodel layer
The Android application will have two view models, PostViewModel
to represent a Reddit post in the list (of which there will be many), and FeedViewModel
as a parent viewmodel to represent the feed of posts.
PostViewModel
is very simple, exposing only the necessary fields that the view needs to display the post. It also transforms the date into a string for display - keeping the display logic out of the view. In this version of the app, it contains no operations - only properties. Operations could include upvoting/downvoting, hiding, saving, and so on - keeping all of the logic nicely separated from the view.
A simple viewmodel such as this may seem a little overkill, why not just use the model class? It demonstrates good encapsulation, no unnecessary fields or operations are exposed to the view. Because the view does not directly depend on the model, we can easily change the application into a reader for another platform, such as Hacker News, without having to change the view. The application could even be extended to show posts from multiple sources, by writing an additional constructor to create the viewmodel from a Hacker New post model.
public class PostViewModel { private String id; private String title; private String author; private String thumbnailUrl; private String createdOn; private String subreddit; private String domain; private int numComments; private int score; public PostViewModel(RedditLink redditLink) { this.id = redditLink.getId(); this.title = redditLink.getTitle(); this.author = redditLink.getAuthor(); this.thumbnailUrl = redditLink.getThumbnail(); this.subreddit = redditLink.getSubreddit(); this.domain = redditLink.getDomain(); this.numComments = redditLink.getNumComments(); this.score = redditLink.getScore(); PrettyTime pt = new PrettyTime(); this.createdOn = pt.format(redditLink.getCreated()); } }View on GitHub
FeedViewModel
is a bit larger, and contains a single operation - loadMorePosts()
. This method will initially load the first page of content from the API. Subsequent calls will load further pages of content, using the stored after
token. The isLoadingSubject
emits true or false to let the view know when loading has taken place - so that the view can show a loading indicator.
public class FeedViewModel { private RedditClient redditClient; private int pageLimit; private String afterToken; private BehaviorSubject<List<PostViewModel> postSubject = BehaviorSubject.create(new ArrayList<>()); private BehaviorSubject<Boolean> isLoadingSubject = BehaviorSubject.create(false); @Inject public FeedViewModel(RedditClient redditClient) { this.redditClient = redditClient; this.pageLimit = 25; } public Observable<List<PostViewModel> loadMorePosts() { // Don't try and load if we're already loading if (isLoadingSubject.getValue()) { return Observable.empty(); } isLoadingSubject.onNext(true); return redditClient .getTop(afterToken, pageLimit) // Safe to cast to RedditListing, as this is always returned from top posts .cast(RedditListing.class) // Store the after token, so we can use it to get the next page of posts is a subsequent load .doOnNext(listing -> afterToken = listing.getAfter()) // Flatten into observable of RedditLinks .map(RedditListing::getChildren) .flatMapIterable(list -> list) .filter(object -> object instanceof RedditLink) // Transform model to viewmodel .map(link -> new PostViewModel((RedditLink) link)) // Merge viewmodels into a single list to be emitted .toList() // Concatenate the new posts to the current posts list, then emit it via the post subject .doOnNext(list -> { List<PostViewModel> fullList = new ArrayList<>(postSubject.getValue()); fullList.addAll(list); postSubject.onNext(fullList); }) .doOnTerminate(() -> isLoadingSubject.onNext(false)); } public Observable<List<PostViewModel> postsObservable() { return postSubject.asObservable(); } public Observable<Boolean> isLoadingObservable() { return isLoadingSubject.asObservable(); } }View on GitHub
Building the view layer
The view is very simple, comprised of two Android XML layouts. The activity layout ( view on GitHub) is made up of a RecyclerView, for showing the list of posts. The post layout ( view on GitHub) is a CardView
reused for each post displayed in the RecyclerView.
The app contains a single activity, FeedActivity
( view on GitHub). As you will see, this is kept very thin - only containing code for setting up the views, and binding the viewmodel to it.
The following is a snippet from FeedActivity
which shows the view being bound to the viewmodel's properties and operations.
// Bind list of posts to the RecyclerView viewModel.postsObservable().observeOn(AndroidSchedulers.mainThread()).subscribe(postAdapter::setItems), // Bind loading status to show/hide loading spinner viewModel.isLoadingObservable().observeOn(AndroidSchedulers.mainThread()).subscribe(this::setIsLoading), // Trigger next page load when RecyclerView is scrolled to the bottom infiniteScrollObservable.subscribe(x -> loadNextPage())View on GitHub
Firstly, the viewmodel emits the list of posts via postsObservable
, which the activity binds to the RecyclerView adapter's setItems
method - meaning the view is updated to reflect new posts being loaded automatically. Secondly, the loading indicator for the view is bound to the viewmodel's isLoadingObservable
, automatically updating the indicator every time there is a change in the loading state. Finally, the inifiniteScrollObservable
emits whenever the Recyclerview is scrolled to the bottom, and is bound to the viewmodel's operation loadNextPage
.
Testing
I've mentioned a few times throughout this post that MVVM promotes testability of the application, allowing the view logic to be tested independently of instantiating a view. To demonstrate this, there is the set of unit tests in FeedViewModelTest
( view on GitHub), which covers testing of FeedViewModel
. Mockito is used to mock the RedditClient
, so that the tests do not depend on real API calls - responses are instead loaded from sample JSON files in the project.
Rounding up
While not quite as nice in Java as in C#, mainly due to not being able to directly observe a property for changes and instead having to use a 'subject' to publish changes, MVVM definitely helps to create a cleaner architecture on Android when followed. Google released their Data Binding library last year, which can be used as an alternative to RxJava - it allows bindings to be written directly in XML. It does however require that viewmodels either extend BaseObservable
or wrap fields in ObservableField
. I believe that keeping all data binding in one place, the activity or fragment, helps to keep things more organised and readable, instead of scattering bindings across XML. Keeping bindings in the activity or fragment allows you to utilise the power of RxJava in the view.
Depending on interest, future posts may look at further examples of MVVM implementation, which may include:
- Using factories to build view models - such as when tapping on a post to open a new activity, built with a view model for that post
- Using commands to perform operations, disabling buttons until complete
- Forms, with validation
- MVVM on iOS, in Objective C
Comments
There aren't any comments yet. Leave one below.
Leave a comment