Kotlin object + Serialization might cause bugs
Sometimes things looks simple and we use it many times but there migth some tweaks associated with it which might cause bugs in certain situations.
Today, we are going to see one such thing. Which is combo of kotlin objects and java serialization.
Kotlin object
Kotlin simplified management of singleton classes and its objects.
// Kotlin
object Hello
vs
// Java
public final class Hello {
@NotNull
public static final Hello INSTANCE;
private Hello() {
}
static {
Hello var0 = new Hello();
INSTANCE = var0;
}
}
For singleton classes, object remains same, hence we can compare objects with equal operator `==`.
for example,
object Hello
val ob1 = Hello
val ob2 = Hello
assertTrue(ob1 == ob2)
In usual case we don’t need to compare singleton objects because it’s obviously same but when we have `object` as a
sub-type of `sealed` entity then there is a need to compare it.
sealed class Parent {
object ChildA : Parent()
data class ChildB(val data: Int) : Parent()
}
val parent1: Parent = ... // any child class
val parent2: Parent = ... // any child class
val isEqual = parent1 == parent2
// or
when (parent) {
ChildA ->
...
is ChildB ->
...
}
But above code won’t work always, if `Hello` or `Parent` is `Serializable`.
Let’s see how,
Problematic usage 1
// Hello.kt
object Hello : Serializable
// Extension function in some class.
fun Hello.clone(): Hello {
val byteArrayOs = ByteArrayOutputStream()
ObjectOutputStream(byteArrayOs).use {
it.writeObject(this)
}
return ObjectInputStream(ByteArrayInputStream(byteArrayOs.toByteArray())).use {
it.readObject() as Hello
}
}
// Use it somewhere
val original = Hello
val cloned = original.clone()
Log.i("TAG", "${original == cloned}")
Even IDE will show `original == cloned` is always true.
But in reality its different objects.
Problematic usage 2
`savedInstanceState` helps us to survive when activity recreates by system cause.
class MyActivity : AppCompatActivity() {
private var globalObj: Hello? = null
@RequiresApi(Build.VERSION_CODES.TIRAMISU)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_my)
globalObj = savedInstanceState?.getSerializable("key-1", Hello::class.java) ?: Hello
}
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
outState.putSerializable("key-1", globalObj)
}
override fun onResume() {
super.onResume()
val localObj = Hello
Log.i("TAG", "Resume global=$globalObj and local=$localObj. Let's Equate ${globalObj == localObj}")
}
}
// Hello.kt
object Hello : Serializable
Let’s analyse this code in different scenario.
- On configuration change (orientation, keyboard, locale, …)
- Activity gets recreated but singleton object still remains same (localObj == globalObj)
- Specify `android:configChanges` : Activity won’t be recreated (localObj == globalObj) - On runtime permission changes from system settings
- Certain permission changed to `Allow`: after coming back to app, localObj == globalObj
- Certain permission changed to `Deny`: after coming back to app, localObj != globalObj
Why???
Because user forcefully `Deny` certain permission, then app needs to handle it gracefully, to make app to handle
permission change,
entire app process will be killed and started again when user visits app again.
This caused kotlin `object` to generate its new object reference (with different hashcode).
But all this thing happened due to system caused reason, which makes savedInstance usable with old reference of `object`.
What does it cause?
- When equating two objects:
Certainly it will fail and `==` returns `false`. Which is a kind of silent bug. - When using in `when` statement:
After retrieving from savedInstance or deserialization if we use such object in `when` then it will fail with `NoWhenBranchMatchedException`.
sealed class Parent : Serializable {
object ChildObject : Parent()
data class ChildClass(a: Int) : Parent()
}
// after deserialization
when (obj) {
ChildObject ->
... // this will fail
is ChildClass ->
...
}
How to resolve it?
1. Use `is` over `==`
This might look absurd but when using `object` + `Serialization` combo then better to use `is`.
sealed class Parent : Serializable {
object ChildObject : Parent()
data class ChildClass(a: Int) : Parent()
}
when (obj) {
is ChildObject ->
...
is ChildClass ->
...
}
Definitely additional comment is required for better understandability.
2. Override equals method of object
This is same as above solution, just we don’t need to mention `is` in every `when` block, developers might forget about it sometimes.
object Hello : Serializable {
override fun equals(other: Any?): Boolean = other is Hello
}
// You got saved yourself from using `is` all times
when (obj) {
ChildObject ->
...
is ChildClass ->
...
}
3. Use `Parcelable` over `Serializable`
Better solution when using in android. As Parcelable is lightweight it will be better solution while dealing with savedInstanceState.
4. Use `readResolve` in `object`
object Hello : Serializable {
private fun readResolve(): Any = Hello
}
function can be private or non-private but return type must be `Any`.
TL;DR
Whenever we see kotlin object + Serializable then treat it as red flag and take extra caution with any of above solution.