Improving LiveData nullability in Kotlin

Improving LiveData nullability in Kotlin
LiveData<Any?>

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.

Replace all instances of the class name.

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.

Replace all the AndroidX imports with your own.

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

Add the import where there are star imports as well

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:

Setting a non-nullable String value to null

The power of Kotlin will now keep us honest:

The Kotlin version will enforce type safety.

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. ;)