11

In our most recent app release we see a handful kotlin.NoWhenBranchMatchedExceptions reported to Fabric/Crashlytics.

This is the code snippet in question:

private lateinit var welcome: Welcome

// ...

welcome.welcomeStateLoginStatus.let {
    val handled = when (it) {
        UnknownUser -> {
            btn_login.visibility = View.VISIBLE
            btn_logout.visibility = View.GONE

            secondButtonFocusedInfoText = getString(R.string.welcome_login_button_info)
            tv_user_description.text = null
        }
        is InternalUser -> {
            btn_login.visibility = View.GONE
            btn_logout.visibility = View.VISIBLE

            secondButtonFocusedInfoText = "Logout"
            tv_user_description.text = "Logged in as internal user"
        }
        ExternalUser -> {
            btn_login.visibility = View.GONE
            btn_logout.visibility = View.VISIBLE

            secondButtonFocusedInfoText = "Logout"
            tv_user_description.text = "Logged in as external user"
        }
    }
}

And the class definitions:

data class Welcome(val welcomeStateLoginStatus: WelcomeStateLoginStatus, val userCanBuySubscription: UserCanBuySubscription? = null) : WelcomeState()

sealed class WelcomeStateLoginStatus() : Serializable
object UnknownUser : WelcomeStateLoginStatus()
data class InternalUser(var user: User) : WelcomeStateLoginStatus()
object ExternalUser : WelcomeStateLoginStatus()

I am puzzled as to how this code can even theoretically throw that exception - as you can see we even introduced the handled value just to force the compiler to make sure that all cases are handled...

Camilo Terevinto
  • 31,141
  • 6
  • 88
  • 120
david.mihola
  • 12,062
  • 8
  • 49
  • 73
  • 1
    No 'is' before `UnknownUser` and `ExternalUser`? – Hong Duan Jun 21 '18 at 06:47
  • These are `object`s, not `data class`es, so the `is` is not needed. – david.mihola Jun 21 '18 at 06:48
  • 1
    I bet this error come from the `: Serializable` and no [`serialVersionUID`](https://stackoverflow.com/questions/285793/what-is-a-serialversionuid-and-why-should-i-use-it). Each subclass has a `serialVersionUID` different. – Lionel Briand Jun 21 '18 at 07:38
  • @LionelBriand: Thank you very much, a colleague also just suggested something along these lines - will look into that! – david.mihola Jun 21 '18 at 07:40
  • @LionelBriand: So, @Hong Duan's suggestion of doing `is` checks instead of equality checks for the `object`s would actually solve the problem? – david.mihola Jun 21 '18 at 07:58

1 Answers1

13

Serialization was indeed the problem:

package com.drei.tv.ui.welcome

import junit.framework.Assert.assertEquals
import org.junit.Test
import java.io.*


class WelcomeStateLoginStatusTest {

    @Test
    fun testSerialization() {
        val original: UnknownUser = UnknownUser

        val copy: UnknownUser = unpickle(pickle(original), UnknownUser::class.java)

        println("singleton: $UnknownUser")
        println("original: $original")
        println("copy: $copy")

        val handled1 = when (copy) {
            original -> println("copy matches original")
            else -> println("copy does not match original")
        }

        val handled2 = when (copy) {
            is UnknownUser -> println("copy is an instance of UnknownUser")
            else -> println("copy is no instance of UnknownUser")
        }

        assertEquals(original, copy)
    }

    private fun <T : Serializable> pickle(obj: T): ByteArray {
        val baos = ByteArrayOutputStream()
        val oos = ObjectOutputStream(baos)
        oos.writeObject(obj)
        oos.close()
        return baos.toByteArray()
    }

    private fun <T : Serializable> unpickle(b: ByteArray, cl: Class<T>): T {
        val bais = ByteArrayInputStream(b)
        val ois = ObjectInputStream(bais)
        val o = ois.readObject()
        return cl.cast(o)
    }
}

Produces the following output:

singleton: com.drei.tv.ui.welcome.UnknownUser@75828a0f
original: com.drei.tv.ui.welcome.UnknownUser@75828a0f
copy: com.drei.tv.ui.welcome.UnknownUser@5f150435
copy does not match original
copy is an instance of UnknownUser

junit.framework.AssertionFailedError: 
Expected :com.drei.tv.ui.welcome.UnknownUser@75828a0f
Actual   :com.drei.tv.ui.welcome.UnknownUser@5f150435

As for solutions: Either implement Serializable properly or just use an is check instead of an equality check.

Thanks to Lionel Briand and Hong Duan for pointing us in the right direction and to Jason S for the code of pickle and unpickle posted in this answer

david.mihola
  • 12,062
  • 8
  • 49
  • 73