Effective Use of ViewModel with LiveData Builder & Transformations on Android

Android VM LiveData Transformation

When we think about MVVM, the first thing that comes to our mind about the job of ViewModel is to fetch data from a model layer. I did some research on how to make the best use of ViewModel and encountered Architecture related classes such as Transformations and LiveData builder. Today, let me introduce some use cases of them with the app that calls TwitterAPI.

MVVM data flow and ViewModel

How to implement ViewModel which mediates View (presentation) and Model (data) is very important in terms of MVVM.

ViewModel works just fine by making it catch some actions on View, or retrieve some data from Model. However, optimizing the management of internal LiveData properties could be a struggle.

In some other cases, ViewModel could work as a data holder like savedInstanceState, by taking advantage of VM’s nature which is to keep living until its owner dies. Also, this allows multiple Fragments to refer to the same ViewModel, which is very convenient.

ViewModel’s Data

By the way, you might be wondering why VM’s data is not cleared even when a structural change like a screen orientation shift caused onDestroy on Activity. This is because onDestroy has two main causes and VM’s data is cleared only when isFinishing is true on onDestroy, which happens when the os kills the Activity.

Now, moving on to the example app.

The Tweets Search App

Basic flow

View
↑↓ ( LiveData<UIData>
ViewModel (Translate or Filter)
↑↓ ( ModelData
Model

As shown above, ViewModel receives the data from Model ( repository ), translates it into a View-friendly format, and passes it to View as LiveData.

Structure

  • InitializeFragment which obtains BearerToken with OAuth2
  • MainActivityFragment on which we search tweets

Fetching the Token

First, let’s get the bearer token we need to call the search API. In real life, we would probably store it on a config XML or something and load it depending on the building environment, or just the backend will manage everything. However, I thought this would be a good example, so I made it interactive.

Once it’s initialized, the token will be stored in Preference.

In this screen, InitializeFragment is asking the API key pair if there is no saved token in Preference. We will save the token if the pair works fine on calling OAuth API, close the Fragment, and open the search screen.

oauth app top

Flow

InitializeFragment - fetchToken()
↑↓ observe: LiveData<NetworkState>
MainViewModel - getBearerToken()
↑↓ Coroutine: TwitterBearerTokenResult
TwitterBearerTokenRepository - getToken()

The key point here is that the type of data being observed is not Token but NetworkState.

Classes

TwitterBearerTokenResult is a class that wraps Token and NetworkState. The View that observes NetworkState will show progress, results, and errors based on the state.

The view does not need the token

The repository makes HTTP requests to fetch the token. It also manages the NetworkState since it has the request state. However, if it returned this result directly to the view, the token will be included about which the view does not care. Furthermore, it would be a wrong task for a view to save the token to Preference. The view only cares about if the token is successfully obtained or not.

LiveData Builder

The LiveData builder is here to save the day. With this builder, you can call repositories’ suspend functions with a designated CoroutineScope, and return the state/value as a LiveData. The callback passed in the argument would be called when the LiveData has active observers.

A Function Exposed to the UI - MainViewModel.kt

In this example, LOADING would be fired first. The repository’s Coroutine function would make a request to fetch a token while the UI is showing a loading screen. Then, the result will be reflected on the NetworkState. The token would be saved on Success and Error logs would be printed. But the point here is that only the NetworkState would be passed to the views.

The Caller Side - InitializeFragment.kt

Now, the view can change the UI based on the state, which is the only thing it observes.

Fetching Tweets

Next, we will fetch tweets through the search API and render them with RecyclerView.

We want the ViewModel to hold on to the tweet list even when the Fragment gets recreated on orientation shifts. So, the Fragment will observe the list in onCreate to always be aware of the VM’s LiveData.

tweets feed

Flow

MainActivityFragment - fetchTweets()
↑↓ observe: LiveData<TweetDataResult>
MainViewModel - search()
↑↓ Coroutine: TweetDataResult
TwitterBearerTokenRepository - getToken()

This time, the same generic type is used. What we need to figure out is how to update the list and keep the list on recreation of the Fragment.

Data Classes

MutableLiveData & MediatorLiveData

A quick solution for this situation could be: MutableLiveData(_tweets) for updating tweets internally, LiveData(tweets) for exposing to View to observe, and MediatorLiveData for repository calling on search events.

The Search Function - MainViewModel.kt

This would work ok but is not quite a beautiful solution. It feels redundant that MediatorLiveData is used inside search() just to make the exposed LiveData immutable, even though it’s not translating the data or mediating LiveData sources. Would there be a better way to update the exposed LiveData on search()?

Transformations

Transformations(ref link) is here for the better solution. This class has functions like map() and switchMap(). Though they are very similar, let’s look into the details.

Transformations.java

As you can see, they are both a function that returns LiveData with a specific generic type. The first argument is a LiveData which serves as a trigger, and the second one is a receiver function that takes the returned value of the trigger. On map(), this function needs to return the generic value, and for switchMap() the return value will be LiveData.

You might think this is similar to the previous implementation of MediatorLiveData, and it sure is returning MediatorLiveData. So, it is technically the same except for the fact that takes two steps. The explanation for switchMap() is below. (validation is ignored)

switchMap Flow

  1. Create MediatorLiveData(result) and returns it
  2. addSource() the first argument LiveData(trigger) to result
  3. On trigger value updates, invoke the second argument callback with the value and addSource() the returned LiveData(mSource) to result
  4. Update the MediatorLiveData(result) value on LiveData(mSource) updates to notify its observers

Now, let’s refactor the MainViewModel with this.

The Search Logic - MainViewModel.kt

Now, this looks cleaner. The MutableLiveData searchWords is the trigger, and we only expose tweetDataResults to the view. This list is the MediatorLiveData returned from Transformations.switchMap(searchWords), which the view can observe.

The trigger value will be updated on search actions, and this notifies the switchMap and the lambda function repository.loadTweets() runs. Finally, the view will update the UI when the repository returns a result.

The Caller Side - MainActivityFragment.kt

The search action would only update the search words. At this point, this feels clean enough for me.

TL;DR

The use of ViewModel is optimized by using LiveData builder and Transformations. The beauty of MVVM is that a view receives optimized data by ViewModels even when the Model returns more complex and various data.

The git repository of this app is here.

References

COPYRIGHT © 2023 Kohei Ando