kotlinandroid-livedatakotlin-interopkotlin-null-safetykotlin-java-interop

How can an exhaustive when throw NoWhenBranchMatchedException?


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 -> {}
  }
}

Solution

  • Why it can happen

    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 or T?" (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)

    What you can do to prevent it

    With compile-time safety

    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
      }
    })
    

    Without compile-time safety

    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.