Get your copy of the book

Transforming the Purchasing Experience

Download
Ebook Retail Transformation Technology

Implementing Custom Architecture based on Model-View-ViewModel with ViewStates

  —  
 read

To shape the future of Android development, Google released a set of libraries, tools and guidelines under the name Android Jetpack. An essential part of Jetpack are the Architecture Components.

Architecture Components are a set of libraries intended to facilitate designing and developing robust, testable and maintainable apps. We’ll take a look at the two most commonly used, ViewModel and LiveData, and how our team used them to implement our variant of the Model-View-ViewModel (MVVM) architecture pattern.

Trouble working with lifecycles

Developers often run into difficulty when working with Activities and Fragments in determining the correct callback method to use to perform an action such as:

  • Saving and restoring the state of the individual views.
  • Starting and canceling network requests, long-running jobs.
  • Opening and closing database connections.
  • Releasing resources kept in memory (media, bitmaps, buffer streams).

Common issues include:

  • Knowing when to release resources, since an Activity’s onDestroy callback is not guaranteed to be called.
  • Having to reinitialize presentation objects (Presenter or Controller) and their dependencies after each configuration change.
  • Properly saving and restoring the full state of the screen during configuration changes. While saved instance state bundles, retained configuration instance objects, headless fragments, repositories, caches, etc. were used for this, fully storing and restoring the state was never trivial.
  • Committing fragment transactions in asynchronous callbacks, while an activity is Started, but not Resumed.

The ViewModel and LiveData components are lifecycle-aware, and help us get around issues like these.

Model View View-Model

The ViewModel is an essential component of the MVVM architectural pattern, commonly used for designing mobile and desktop applications.

Architecture Components only provides us a ViewModel implementation, and we’re free to implement the View and Model in any way we see fit. This ViewModel class is responsible for the presentation data and logic for its scope. A ViewModel's scope can be either an Activity or a Fragment.

We instantiate a ViewModel in its scope with a ViewModelProvider, during the scope's creation.

When an Activity or Fragment is recreated due to a configuration change, it can immediately access the data stored in the ViewModel. We no longer have to save and restore the state by serializing it to a bundle, which might work well for small sets of primitive data, but is not that practical for big collections, images, audio streams, etc.

ALT goes here

Another important component of the MVVM architecture pattern is exposing the data from the ViewModel to the View (Activities, Fragments, custom Views) by the use of the Observer pattern.

LiveData is an implementation of the pattern, and it is an observable data stream that is Lifecycle aware. To those familiar with RxJava, LiveData has some limited set of the functionality of an RxJava Observable. With the inclusion of some additional libraries, like Lives, we can get most of the functionality we’re used to in RxJava, like filtering, combining and transforming operators.

A View will start observing an exposed LiveData and modify its UI according to the values it observes. While we can set a LiveData’s value at any time, the LiveData will only notify the observing View, when it is in at least its Started state.

Improving on the basic MVVM pattern

In basic MVVM implementations, each piece of data that needs to be bound to a UI element should be exposed though its own LiveData, matched with an Observer instance in the View. For simple screens this wouldn’t be much of an issue, but as complexity grows so would the number of LiveDatas and Observers active at the same time, which could potentially impact performance.

At Infinum, to go around this limitation we have borrowed the concept of a ViewState, used in MVI (Model View Intent), another popular architecture pattern for mobile and web applications.

A ViewState object contains all the data required to describe the state of every subcomponent of the View at once. Implementing a ViewState in Kotlin is trivial, it can be done with a data class, containing only primitives and other data classes, where all the data classes would have the same restriction, all the way down in the structure.

Data classes provide equality comparison and toString out-of-the-box. Now, we only need to expose a single LiveData that holds the current ViewState.

Let’s say we are to implement a screen with a list of TV shows that the user is currently watching. The ViewModel contains a private ShowsState MutableLiveData so it can change its value, but it is exposed as a regular LiveData so it is protected and cannot be changed from outside.

private val showsState = MutableLiveData<ShowsState>()
val state: LiveData<ShowsState> = showsState

And the states could be:

class ShowsState

object NoShowsFound : ShowsState()
data class ShowListFetched(val shows: List<Show>) : ShowsState()
data class ErrorFetchingShows(val errorMessage: String) : ShowsState()

Another Kotlin language feature that comes in handy with ViewStates is sealed classes. If we make our screen’s base state class sealed, then we are forced to enumerate all the possible states the screen can be in, in one file. The enumeration helps us when we’re using the state object as a parameter of a when statement, a compile-time check tells us if we’ve covered all the states.

Additionally, if we nest the extending state classes in the base class, we can use the IDE autocomplete feature, we only need to type in the name of the base state class, and we get a full list of all possible states.

If we applied the nesting we’d have these states:

sealed class ShowsState {
   object NoneFound: ShowsState()
   data class Fetched(val shows: List<Show>): ShowsState()
   data class Error(val message: String): ShowsState()
}

Which we would observe:

fun observeState(state: ShowsState) {
   when(state) {
       is ShowsState.NoneFound -> displayNoneFound()
       is ShowsState.Fetched -> displayShows(state.shows)
       is ShowsState.Error -> displayErrorState(state.message)
   }
}

A very common functionality we have in almost every feature is to display the progress of some action, or to notify the user of an error occuring in some specific way. To cut down on writing almost identical variants of the same concept, and for the sake of consistency throughout all features in the app, we define a set of common states, which coexist with the set of states specific to each feature.

A common ViewState object contains the current error state and loading state:

data class CommonState(
   val loadingState: LoadingState = LoadingState.Idle,
   val errorState: ErrorState = ErrorState.NoError
)

sealed class LoadingState {
   object Idle : LoadingState()
   object Loading : LoadingState()
}

sealed class ErrorState {
   object NoError : ErrorState()
   object UnknownError: ErrorState()
   data class Error(val message: String) : ErrorState()
}

With a combination of the feature-specific and common ViewState-s, we can instruct the View from the ViewModel to completely form the UI. The View itself can be destroyed and recreated an arbitrary amount of times, due to whatever reason, but we’re still able to recreate it consistently, and without having to save and restore instances, and manage states on several different levels like in the good old days.

On the other hand, some information we do not want to embed in the ViewState. Instead, we want to show it only once.

For that purpose we have Events, and use them mostly for displaying alerts, snackbars or other kinds of on-screen notifications. Events are also exposed to the View through a SingleLiveEvent, an extension of LiveData based on SingleLiveEvent. This live data emits any event only once to its subscribers, and will not re-emit in case the observer’s lifecycle owner is recreated due to a configuration change.

An extensible base for our Views and ViewModels

A common practice we have at Infinum is creating base implementations for the most commonly used components. For the MVVM implementation, we create a BaseFragment and BaseViewModel.

The BaseViewModel class exposes three data streams as LiveData-s, specifically: - Feature state - Feature events - Common state

open class BaseViewModel<State: Any, Event: Any> : ViewModel() {
   private val stateLiveData: MutableLiveData<State> = MutableLiveData()
   private val commonStateLiveData: MutableLiveData<CommonState> = MutableLiveData()
   private val eventLiveData: LiveEvent<Event> = LiveEvent()

   fun stateHolder(): LiveData<State> = stateLiveData
   fun commonStateHolder(): LiveData<CommonState> = commonStateLiveData
   fun eventHolder(): LiveData<Event> = eventLiveData
}

It also contains convenience methods for changing the common ViewState.

protected fun showLoading() {
   commonState = commonState.copy(loadingState = LoadingState.Loading)
}

protected fun hideLoading() {
   commonState = commonState.copy(loadingState = LoadingState.Idle)
}

protected fun showError(message: String = "") {
   commonState = commonState.copy(errorState = ErrorState.Error(message))
}

fun dismissError() {
   commonState = commonState.copy(errorState = ErrorState.NoError)
}

The BaseFragment class that contains code that is shared by all the Fragments in the application, including basic UI setup, dependency injection, shared resources and utilities, and most importantly the wiring between the View and the ViewModel.

It is provided with an instance of its ViewModel, and it starts observing the streams from the ViewModel, as soon as it is ready.

abstract class BaseFragment<State : Any, Event : Any>() : Fragment() {
    abstract fun provideBaseViewModel(): BaseViewModel<State, Event>?

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        provideBaseViewModel()?.apply {
            stateHolder()?.observe(viewLifecycleOwner, Observer { state -> handleState(state) })
            commonStateHolder()?.observe(viewLifecycleOwner, Observer { state -> handleCommonState(state) })
            eventHolder()?.observe(viewLifecycleOwner, Observer { event -> handleEvent(event) })
        }
    }

    abstract fun handleState(state: State)
    abstract fun handleEvent(event: Event)
}

Implementing a feature’s View and ViewModel

The BaseFragment contains an implementation for handleCommonState, but the extending feature fragments must implement the handleState and handleEvent methods. User interactions are signaled from the View to the ViewModel through methods that the ViewModel exposes.

class ShowsFragment : BaseFragment<ShowsState, ShowsEvents>() {
    addNewShowButton.setOnClickListener {
        viewModel.addNewShow(showTitle)
    }

    showsSwipeRefreshView.setOnRefreshListener {
        viewModel.refreshShowsData()
    }
}

It is very important to be consistent and thorough when defining the feature specific ViewState-s.

Let's examine the case were we add a search functionality to the Shows screen. We could leave the states as they are, and simply pass the search query from the View to the ViewModel, after which it will process the query, and return a new state containing a list of shows passing the search criteria, and we’d get a working search screen. But it will only work up until the point the user rotates their device.

After the fragment is recreated, the search query will be lost, because it's not stored in the state. If we add it to the state, the complete version of the state class would look like this:

sealed class ShowsState { 
   abstract val searchQuery: String

   data class NoneFound(override val searchQuery: String): ShowsState()

   data class Fetched(
       val shows: List<Show>,
       override val searchQuery: String
   ): ShowsState()

   data class Error(
       val message: String,
       override val searchQuery: String
   ): ShowsState()
}

ALT goes here

The ViewState-s now completely describe the screen, and the screen will be completely restored upon recreation.

A future-proof architecture

It is important to note that it is not a silver bullet. If we consider the search screen, the search query field is modified by the fragment due to state changes, and is modified by the user, often simultaneously, which could lead to inconsistencies in the text content, cursor position, selection, etc.

No one-size-fits-all solution exists, and each situation should be addressed individually to maintain consistent UX.

This was just an overview to Infinum's implementation of the MVVM architecture pattern, and insight into some of the motivations behind the design decisions that we’ve made. The more we use the architecture pattern, the more we’ll see where it shines, and where it lacks, and we'll continue to improve it.

Stay tuned!

Fly me to the space Jelena Njeguš created for the cover illustration.