Improving LiveData nullability in Kotlin

Since its initial release more than two years ago, Android’s LiveData
has been an incredibly popular, simple approach to safe-ish reactive programming. However, even though it was announced on the same day as Kotlin support, it is a Java library and doesn’t have perfect Kotlin interop.
Part of the issue is in its observe
function, which in Kotlin looks like this:
val someLiveData : LiveData<String> = ...
someLiveData.observe(this, Observer { someString: String? ->
// the value from the callback is always nullable
stringWhichIsNotNull = someString ?: "placeholder message"
}
However, with the upcoming livedata-ktx 2.2.0 (currently in rc03), we have a new observe
extension function, which allows us to write more idiomatic Kotlin:
val someLiveData : LiveData<String> = ...
viewModel.someLiveData.observe(this) { someString: String ->
// the value from this lambda is inferred as non-nullable
stringWhichIsNotNull = message
}
Yay! Finally, we don’t have to use double-bangs or unnecessary null checks! We can write real code which always handles nullability correctly. Anywhere some library is providing us with a LiveData<T>
, such as Room or a coroutines utility function with Flow<T>.asLiveData()
, we can be confident that the value will adhere to proper Kotlin ideas about nullability.
But what about homemade LiveData?
Sometimes we don’t get our LiveData<T>
from a library. Sometimes we have to create them ourselves. And while this new observer function allows us to lose some boilerplate on the receiving end, it doesn’t solve all the problems:
val someLiveData = MutableLiveData<String>()
val currentValue = someLiveData.value
println(currentValue) // null
In this snippet we see a problem. The type is said to be String
, but the value
property access (which is getValue()
in Java) still returns a nullable value. We can see why by looking at the Java:
package androidx.lifecycle;
@Nullable
public T getValue() {
Object data = mData;
if (data != NOT_SET) {
return (T) data;
}
return null;
}
But there’s a more pernicious problem. Even though we’ve declared this MutableLiveData<String>
as being non-nullable, we’re able to set its value to null!
val someLiveData = MutableLiveData<String>()
// this compiles!!
someLiveData.value = null
Same as before, this is because of Java. It has no way to enforce proper nullability.
package androidx.lifecycle;
public class MutableLiveData<T> extends LiveData<T> {
...
@Override
public void setValue(T value) {
super.setValue(value);
}
}
We can fix it!
Since LiveData
is just a Java class, it’s open
(non-final
) by default. We can simply extend the class, then add public
modifiers to the two setters to make it mutable.
@Suppress("UNCHECKED_CAST")
class MutableLiveData2<T>(value: T) : LiveData<T>(value) {
override fun getValue(): T = super.getValue() as T
public override fun setValue(value: T) = super.setValue(value)
public override fun postValue(value: T) = super.postValue(value)
}
I’ve named this class MutableLiveData2
. This is not a good name, but it does stand out which is kind of what I’m going for.
The “migration” consists of two easy steps with find-and-replace. First, simply replace all instances of the class name. In my case it’s MutableLiveData2
.

Then, update all imports. Note that the last step will have updated all imports as well, so the import you’re replacing is actually an invalid one.

Note that if you have star imports for androidx.lifecycle
, you may need a third step:

Hopefully, you use Android Studio’s “optimize imports” feature when auto-formatting or committing, so that everything gets cleaned up nicely:

So now, whereas before we could make up whatever nullability we’d like:

The power of Kotlin will now keep us honest:

This change is only necessary in places where you’re exposing the mutability of that LiveData
. Since it extends LiveData
, it can be used any place a normal MutableLiveData
can, and of course, it's also a normal LiveData
as well.
So if you’re using LiveData
, I’d recommend you give this a try. If you have a name which is less terrible than MutableLiveData2
, please share.
MutableLiveDataX
has already been rejected. ;)