The View Layer: A Story of States and Views
Introduction
This article is a description of how the view layer of an Android application can be architected and implemented using the Model-ViewModel pattern with a key principle: make the views as declarative as possible. The application I use to expose the ideas is MoviesPreview, which is an application that allows a user to browse the data exposed by The Movie DB API to see different information about movies, actors/actresses, and some other features. The full code of the project can be found in this Github repository.
Principles
I’ve based the implementation of the view layer using MVVM, following the basic principles or guidelines:
- The less code, the better. The view layer of an application should only take care of one thing and one thing only: render the view state.
- The states that a view can present at any given moment should be as clear as possible and it should be as structured as possible in order to make it easy for the developer when reading the code.
- The view layer code should be as declarative as possible. This means that no control block should be found in the view layer.
View state and its representation
The state of a View
is defined as the visibility and the content of the View. The details of positioning, margins, sizes, etc. are not part of the state description of a view. Those are properties that are intrinsic, independently of the state it can assume.
Ideally, Each Fragment/Activity has a one to one mapping with a ViewState class. EachViewState
class contains a representation of the state of every individualView
present in the view hierarchy of the layout XML file of the Fragment/Activity. This way is really easy to reproduce any state of the application that is presented to the user without the need to reproduce the inner state of the application (the Model). It also allows making a declarative representation of what the Fragment/Activity can show to the user to facilitate reading the code.
For simple view hierarchies, this approach is easy to apply, follow, and read. Of course, in the real world view hierarchies are far more complex than this. For these situations, we can take this representation to a deeper level of granularity by wrapping similar properties of the state in other view states:
On occasions, a single ViewState
class per Activity/Fragment is hard to achieve since it might complicate the logic needed to update specific sections of the view hierarchy. For instance: in MoviesPreview, there is a screen that shows the details of a movie. It contains an image with the poster of the movie plus the synopsis and a couple of other details that are specific to the movie. It also gives the user the possibility to perform actions with the movie: add it to the favorite movies list, add it to a watch-list or rate the movie.
It was really hard to maintain the whole state of the screen in a single class. Even more, an animation is executed every time the user clicks in the button with the icon. Having to recreate and re-render the whole screen for every user touch on the button was extremely hard to model and control. The decision then was to break the view into different ViewStates
, each one of them related to a specific context: one for the static information of the screen and one for the dynamic section. The result was a much simpler interaction between the View Layer and the View Model:
In order to have clear transitions between one ViewState
and another (for instance, when the visibility of a view changes), each ViewState
class has a set of factory methods defined. By invoking those factory methods, we make the code to update the view state fairly simple and we make sure that a developer does not need to explore a whole ViewModel in order to detect each state a View can go through.
ViewModel to control the state
The other important part of the UI architecture is the ViewModel. As the name says it explicitly, all ViewModels
in the architecture are implementing the ViewModel
class from the Android Architecture Components in order to make them lifecycle aware and to avoid complications of handling the destruction and recreation of views and its containers.
Besides this (very important) inherited responsibility, the ViewModel
has one very specific responsibility that is control flow. It takes care of executing the UseCases available in the Domain Layer (the Model in MVVM) and sends the ViewState
to the View Layer to render.
The ViewModel
API is fairly simple: it has only one output that is the ViewState
in the form of Android’s LiveData to take a reactive approach and as many inputs as needed to intercept user actions. This means that for every view the user can interact with, there is a corresponding public method in the ViewModel
that the View Layer takes care of mapping. That way, for every user action, there is a consequence that handled by the ViewModel
. That consequence might be either a state update or a request to navigate to another screen.
So, basically, every time the user performs an action (like viewing the screen for the first time) the View Layer executes a method in the ViewModel
that triggers a state update in the application.
There are two extra responsibilities that the ViewModel
have:
- Map domain entities resulted from UseCase execution into ViewStates: the
ViewModel
makes the translation between the result of the UseCase and the ViewState in order to send the ViewState for rendering. We could introduce a state-reducer or a mapper to have this responsibility, but I decided to minimize the number of dependencies. - Control the thread in which each action is executed: this is kind of an inherited responsibility. Since the application is using Koltin’s coroutines to execute long-term actions, the
ViewModel
base class from the Android Architecture Components comes with built-in logic to handle these coroutines.
Data Binding as the glue between Views and ViewState
Android Data Binding library is used to transform ViewState
classes into View
properties, without the need to have that responsibility in the Fragment/Activity. By using this technique, the code to render a ViewState
can be as simple as this:
private fun renderViewState(viewState: PersonViewState) {
setScreenTitle(viewState.screenTitle)
viewBinding?.viewState = viewState
}
This is because the variable
used in the XML declaration of the Views it is no other than the ViewState
that represents the entire hierarchy of the layout. All we have to do in order to render the view state is pass the new ViewState
into the viewBinding
created when the View is initialized and the rest is done in a declarative way in the layout XML:
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<data>
<variable
name="viewState"
type="com.jpp.mpcredits.CreditsViewState" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.jpp.mpdesign.views.MPErrorView
android:id="@+id/creditsErrorView"
android:layout_width="0dp"
android:layout_height="0dp"
app:animatedVisibility="@{viewState.errorViewState.visibility}"
app:asConnectivity="@{viewState.errorViewState.isConnectivity}"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:onRetry="@{viewState.errorViewState.errorHandler}" />
<ProgressBar
android:id="@+id/creditsLoadingView"
style="@style/MPProgressBar"
app:animatedVisibility="@{viewState.loadingVisibility}"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/creditsRv"
android:layout_width="0dp"
android:layout_height="0dp"
app:animatedVisibility="@{viewState.creditsViewState.visibility}"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:listitem="@layout/list_item_credits" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
One rule is defined and respected as a rule of thumb in order to have this interaction clear:
- Data Binding must be used to map
ViewState
properties withView
properties without the usage of Data Binding expressions.
By respecting this rule, we ensure that there are no business rules hidden in the XML layouts.