I observe a LiveData with an exhaustive when
statement, how can it throw a NoWhenBranchMatchedException
at runtime? Wasn't exhaustiveness checked for at compile time?
enum class MyEnum { Foo, Bar }
liveData.observe(viewLifecycleOwner) { enumInstance ->
val instance: MyEnum = when (enumInstance) {
is Foo -> {}
is Bar -> {}
}
}
Because LiveData is an API written in Java, and any type can be null in Java, Kotlin's null-safety is not enforced at compile time. The IntelliJ IDEs refer to these types with a !
suffix:
T!
means "T
orT?
" (source)
enumInstance
may be null
, but Kotlin doesn't do any null checks on it because Observer
is a Java interface. That's why this when
statement is still considered to be exhaustive by the compiler, even though it isn't. Setting the value of the LiveData from the example to null will cause a NoWhenBranchMatchedException
to be thrown at runtime.
liveData.value = Foo // Perfectly safe at runtime
liveData.value = null // NOT safe at runtime
liveData.observe(viewLifecycleOwner, Observer { enumInstance ->
// enumInstance is "MyEnum!" (nullability unknown to Kotlin)
val instance = when (enumInstance) {
is Foo -> {}
is Bar -> {}
}
})
Any reference in Java may be null, which makes Kotlin's requirements of strict null-safety impractical for objects coming from Java. Types of Java declarations are treated in Kotlin in a specific manner and called platform types. Null-checks are relaxed for such types, so that safety guarantees for them are the same as in Java [...] (source)
If it comes from Java, either check for null or assume it to be nullable at any time. Make the type explicit to Kotlin.
What we were doing:
val unknownNullability = javaApi.getString() // "String!"
unknownNullability.split(',') // Runtime error
What we should do:
val knownToBeNullable: String? = javaApi.getString()
knownToBeNullable.split(',') // Compile-time error
Fixing the example code:
liveData.observe(viewLifecycleOwner, Observer { enumInstance: MyEnum? ->
// Now I know I may receive null
// This will fail at compile-time:
when (enumInstance) {
is Foo -> {}
is Bar -> {}
// null case is required now
}
})
You can simply avoid using null
with LiveData
(and other places with Java interop). Observers won't receive null unless you post it.
liveData.value = Foo
liveData.observe(viewLifecycleOwner, Observer { enumInstance ->
// I'm sure I won't receive null
})
However, code changes constantly. Even though your code today never posts null
, it may come to post it one day, and with this approach any nullability errors will only come up at runtime.