Stateful.Live.Data

When Meta Met Data

Giora Shevach
Published in
5 min readDec 25, 2019

--

Releasing LiveData was one of the greatest things Google has done in recent years in the Android domain. This technology sure brings new capabilities when it comes to propagating data across an app, and you can read more about it in this post I’ve written in the past, as well as in the official documentation.

However LiveData isn’t always enough. After a while of utilizing LiveData and its power in our day-to-day work here at ClimaCell, we started to realize that it can’t solve all of our problems when it comes to data observation across our app and codebase.

Take the most straightforward scenario for example: loading some data from the network and propagate it to the UI component. This scenario is a classic use-case where LiveData can assist us — just set the new data to the LiveData instance, and the observing UI component gets new data changes updates seamlessly. Something like this:

class MyRepo {
val myData: MutableLiveData<MyData> = ...

fun fetchFromNetwork() {
// do some network stuff…
myData.value = networkResponse.data
}
}

class MyViewModel : ViewModel {
val myData: LiveData<MyData> = myRepo.myData
}

class MyActivity : Activity() {

private lateinit viewModel: MyViewModel

override fun onCreate(savedInstanceState: Bundle) {
super.onCreate(savedInstanceState)

viewModel = ViewModelProviders.of(this)
.get(MyViewModel::java.class)

observeMyData()
}

private fun observeMyData() {
viewModel.myData.observe(this, Observer { // it: MyData

doSomething(it)
})
}
}

But what about the user experience in such a flow? In the happy-flow, the response is received at some point in time, and the data is swapped in front of the user’s face. Not much of a pleasant UX. In the not-so-happy-flow, there is a network error, and we never set data to the LiveData instance. This means we leave the user with the old data, with no way of knowing that something abnormal has happened.

There are several valid solutions to this problem, solutions we all used before LiveData came into our lives. For instance, implementing an onError listener interface or using an event-bus.

Yet picking them would mean giving up on all LiveData has to offer.

As an alternative, we could use another instance of LiveData, which will represent an error, say LiveData<Exception>. This approach would lead us to observe the two seemingly independent instances of LiveData, and would force us to implement some logic to maintain our states of valid data and error. This logic isn’t very much straightforward when you observe the two instances, and doesn’t scale when you want to support additional states, say “Loading” state with an additional instance of LiveData. And even if you manage to implement it successfully, tomorrow you’ll want the same logic implemented for a similar feature but in a different UI component.

StatefulLiveData for the rescue

We at ClimaCell sought out to create a new solution that would address the limitations of LiveData when it comes to scenarios like the one described above. We started by creating the following StatefulData class:

abstract class StatefulData<T> {

class Success<T>(val data: T) : StatefulData<T>()
class Error<T>(val throwable: Throwable) : StatefulData<T>()
class Loading<T>(val loadingData: Any? = null) : StatefulData<T>()

}

As can be seen in the code above, StatefulData is generic so it supports wrapping any type, and has three concrete subclasses. These subclasses describe states the data can be in — whether it’s retrieved successfully and ready to be used, an error has occurred, or whether the data is still being loaded. In other words — MetaData. Each of the subclasses also defines a property based on the state the class represents — for Success state we have the “data” property to access the data itself. For Error state we have an instance of Throwable for us to know what had happen. And for the Loading state we have a “loadingData” property that can hold partial data for us to use while the data is still loading.

To harness the power of StatefulData class and to overcome situations where LiveData falls short, we then created the following StatefulLiveData:

typealias StatefulLiveData<T> = LiveData<StatefulData<T>>

How is this helpful?

Let’s see how this simple typealias can solve the previous scenario. First, let’s change our code of MyRepo and MyViewModel to use StatefulLiveData:

class MyRepo {
val myData: MutableStatefulLiveData<MyData> = ...

fun fetchFromNetwork() {
// do some network stuff…
myData.value = networkResponse.data
}
}

class MyViewModel : ViewModel {
val myData: StatefulLiveData<MyData> = myRepo.myData
}

Now let’s see how our observation in the MyActivity has changed:

class MyActivity : Activity() {

...

private fun observeMyData() {
viewModel.myData.observe(this, Observer {
// it: StatefulData<MyData>

When (it) {
is Success -> { // it.data: MyData
doSomething(it.data)
}
is Loading -> { // it.loadingData: Any?
showLoader()
}
is Error -> { // it.throwable: Throwable
showErrorMessage()
log(it.throwable)
}
}

})
}
}

Let’s break it down a bit: First, we still can observe our viewModel.myData property, since we saw that StatefulLiveData is also LiveData. Note how now the argument of the Observer lambda (it) is of type StatefulData<MyData>. Since we know all the concrete subclasses of the StatefulData class, we can write a simple when block — we check to see in which of the three states our observed data is, and then act accordingly. This is just how simple it is to answer scenarios like the one we described while still utilizing LiveData’s many advantages.

That is truly the basics of StatefulLiveData.

Besides the basic use, StatefulLiveData has more to offer. It’s equipped with Stateful-Transformations, which allow you to map and switch-map your StatefulLiveData instance, while keeping its state. Additionally, StatefulLiveData supports MediatorStatefulLiveData, that allows adding both LiveData and StatefulLiveData instances as sources.

Armed with many of the features we got used to when using (vanilla) LiveData, as well as additional helpful new features, this simple yet powerful typealias has helped us in many scenarios and gotten deeply into our codebase.

Come and get your Stateful now!

To make use of this tool and read more about its features and usages, go ahead and visit our ClimaCell’s StatefulLiveData library over at Github.

Why choose this over other solutions?

StatefulLiveData isn’t the only way to overcome LiveData limitations. Other common libraries also deal with different states and events. However, no other library enables you to enjoy the many advantages LiveData has to offer, particularly the concept of lifecycle-awareness. In addition, other libraries often come with a steep learning curve, a large package size and no way of retaining your data. Moreover, these libraries are sometimes way more versatile and equipped than you need for your app and scenarios.

The fact that StatefulLiveData, on top of being a LiveData itself, is also easy to use, lean and focused — is what makes it so powerful.

Summary

I hope this post helped you gain some insights regarding LiveData and its limitations, and that you’ll take StatefulLiveData into account when searching for alternative ways to propagate data in your app. If you have any questions or just want to share an idea, feel free to add a comment.

--

--