The View Layer: A Story of States and Views

Introduction

Juan Peretti
The Startup
6 min readJul 16, 2020

--

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.

A basic ViewState representation

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:

Granular ViewState

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:

ViewState breakdown for movie details

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 with View 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.

--

--

Juan Peretti
The Startup

Mobile Software Developer — Passionate traveler