In our most recent app release we see a handful kotlin.NoWhenBranchMatchedException
s 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...
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
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With