When LiveData and Kotlin don’t play well together

Playing well together

The idea of LiveData was an interesting one. Based on the idea of reactive streams, that was on the peak at that time with RxJava plus adding automatic lifecycle handling — a problem on Android. LiveData had bad timing though. It arrived just before Kotlin made its impact in the Android community and sometimes both don't play that nice together. Let’s explore why and what can happen!

The good and the bad

The idea of LiveData was pretty simple: a lifecycle-aware implementation of the Observable pattern. In addition, if you resubscribe, you will get the last emitted value again. It could be compared to a typed version of an EventBus with sticky messages.

This is one of the core features of LiveData. But very soon after developers, including the authors, figured, you don’t always want that behavior. Let’s say you have an error value. Then you probably don’t want to show that error value again after resubscribing, like after rotation of a device — for instance.

One way to solve this is SingleLiveEvent and it’s a good choice for one-off events like errors.

Both worlds

But what if you want a bit of both? You want to have the last value “sticky” plus not showing potential errors multiple times!
In that case, it would be good to remove our error value from LiveData after they have been consumed, right?

Let’s look at the implementation ofLiveData:

The current value is saved on a field:

private volatile Object mData;

Initially, it is set to NOT_SET which.

static final Object NOT_SET = new Object();

Unfortunately, is not public otherwise, we could use it to reset the value.

What now?

If you search for this problem, one of the most suggested solutions is simply setting it to null.

Let’s explore this:

viewModel.results.observe(lifecycleOwner) { result ->
when (result) {
SomeResult.Error -> {
handleError()
viewModel.results.reset()
}
SomeResult.Result -> { handleResult(result) }
}
}

where the ViewModel’s implementation is:

class MyViewModel: ViewModel() {

private val mutableResults = MutableLiveData<SomeResult>()

val results: LiveData<SomeResult>
get() = mutableResults

fun reset() {
mutableResults.value = null
}

}

This works, right?

Congratulations! You just introduced a crash into your code!

But why?

You might not even notice and might roll it out. (If so, please add unit tests next time).

The issue here is that we declared LiveData as non-nullable to the compiler!

Your observer will get notified with this null value and then immediately crash once it gets a value with a KotlinNullPointerException under the hood. There is nothing you can do anymore! It’s part of how Kotlin works!

LiveData is written in Java. It does allow setting it’s value to null. So the authors could add @Nullable annotation as we have them on other Jetpack libraries, right? No, as then you always need to handle nullable types in getValue or the Observer although you clearly might have declared your LiveData as non-null. The Generic is runtime information (for Java), annotations are compile-time information, they can’t solve this for us. And Java simply does not have null in the type system.

It’s not a bug - it’s a feature!

How can you solve this?

An easy one would be to mark your LiveData nullable:

private val mutableResults = MutableLiveData<SomeResult?>()

val results: LiveData<SomeResult?>
get() = mutableResults

Now we can handle that in the observer and only react to non null values. And more important: the compiler forces us to!

Is there another way?

I said earlier that we couldn’t access the original NOT_SET value. It’s not totally true. We could expose it by setting something into the jetpack package:

package androidx.lifecycle

fun <T> LiveData<T>.reset(){
this.value = LiveData.NOT_SET as T?
}

It’s a bit dirty though.

But other than that, is this actually allowed?
It’s at least not forbidden! Even by the newer stricter rules about private APIs for apps on Google Play, it should be fine.

The actual problem is a different one, but most importantly, you rely on an implementation detail of LiveData that is not part of the public API and therefore could change anytime!

Ok, what now?

Another alternative is to wrap the results so you can have your own NOT_SET:

sealed class ViewModelResult {
data class Result(val result: SomeResult): ViewModelResult()
object NotSet: ViewModelResult()
}

We would use NotSet to clear of previous values and then could just ignore that in the Observer.

It’s not the most beautiful but probably the cleanest way of solving this.
This is also recommended by Jose Alcérreca in this article.

Anything else?

Also, try to re-think if you need LiveData in the first place. With StateFlow and ShareFlow Kotlin provides a great alternative.

If you want to stick with LiveData, think if there is a way you can either have sticky or non sticky events instead of a mix?

In that case, think about updating your Linter to add a custom Lint check where our non-nullable live data cannot be set to null and the compiler in that case would cause an error.

Stay alert! This is not a LiveData issue, it could happen with other libraries too. We got very used to the Kotlin compiler warning us. But if it can’t, it might bring you into troubles you won’t even have without it.

--

--

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store