
The topic of data serialization is fundamental for any mobile developer, since it is used for working with the network, the file system, and communication between core components. However, there is one peculiarity in this area that I encountered for the first time in seven years. That discovery is exactly what this article is about — a mixture of two serialization methods, as well as why, to solve this task, it is important to understand how different types of class loaders work. The information in this article will help you gradually migrate to Parcelable in the necessary places without rewriting all classes to the new technology at once.
Since Parcelable is not a general-purpose serialization mechanism (it cannot be used for saving data to disk or for network requests), it cannot fully replace Serializable, but it remains more efficient for the Android environment. These two serialization solutions will stay with us in projects for a long time, which means we need to know how to work with them correctly.
Starting Point
The article was born out of a crash that periodically occurred in the logging system. Let’s figure out what went wrong. Consider a simplified example:
data class ScreenViewModel(
val payload: Any
): Serializable
Within the screen, payload was used either for navigation via deeplink (if a string was passed), or for navigation to another Activity if an Intent was passed. At first glance, it may seem like there is no problem, but let’s look at the declaration of the Intent class:
public class Intent implements Parcelable, Cloneable
This is exactly where the cause of the application crash is revealed — unlike String, Intent is not Serializable. In that case, let’s write code that will allow us to dynamically choose the serialization type and use it while preserving the ability to pass exactly the same Payload into the feature.
data class ScreenViewModel(
val payload: Any
): Parcelable {
public companion object CREATOR : Parcelable.Creator<ScreenViewModel> {
override fun createFromParcel(parcel: Parcel): ScreenViewModel {
return ScreenViewModel(parcel)
}
override fun newArray(size: Int): Array<ScreenViewModel?> {
return arrayOfNulls(size)
}
}
public constructor(
parcel: Parcel
) : this(
payload = parcel.readPayload(),
)
override fun writeToParcel(parcel: Parcel, flags: Int) {
parcel.writePayload(payload, flags)
}
override fun describeContents(): Int = 0
}
In the boilerplate code for the Parcelable implementation, there are two extensions that interest us: readPayload and writePayload.
To dynamically understand which serialization method is needed, it is enough to save the serialization type identifier next to the object.
private const val TYPE_NULL = 0
private const val TYPE_PARCELABLE = 1
private const val TYPE_SERIALIZABLE = 2
public fun Parcel.writePayload(payload: Any?, flags: Int) {
when (payload) {
is Parcelable -> {
writeInt(TYPE_PARCELABLE)
writeParcelable(payload, flags)
}
is Serializable -> {
writeInt(TYPE_SERIALIZABLE)
writeSerializable(payload)
}
null -> {
writeInt(TYPE_NULL)
}
else -> {
throw IllegalArgumentException("Unsupported type for payload")
}
}
}
This way, at the reading stage, we will be able to understand which method was used during writing and use exactly that one.
public fun Parcel.readPayload(): Any {
return requireNotNull(readNullablePayload())
}
public fun Parcel.readNullablePayload(): Any? {
return when (readInt()) {
TYPE_PARCELABLE -> readParcelable<Parcelable>()
TYPE_SERIALIZABLE -> readSerializable<Serializable>()
TYPE_NULL -> null
else -> throw IllegalArgumentException("Unknown payload type")
}
}
Now we only need to implement the readParcelable and readSerializable methods, after which the integration is complete. But despite the simplicity of calling the system methods for reading Serializable and Parcelable, there is still a problem. Let’s look at the code:
public inline fun <reified T> Parcel.readParcelable(): T? {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
readParcelable(T::class.java.classLoader, T::class.java)
} else {
readParcelable(T::class.java.classLoader)
}
}
At first glance, nothing seems unusual until you try to read a class from your application which, at runtime, will be the generalized type Parcelable. Because of this, when the code accesses classLoader in Parcelable::class.java.classLoader, the system will retrieve java.lang.BootClassLoader, which naturally does not contain your application classes (it is the system classLoader of the virtual machine, where system classes are located, including our Parcelable), and therefore deserialization will be impossible. You can read more about different types of classLoaders in the official documentation. One of the solutions is that when working with the generalized Parcelable type, instead of using its classLoader, you can use your application’s classLoader (for example, dalvik.system.PathClassLoader).
public inline fun <reified T> Parcel.readParcelable(): T? {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
if (T::class != Parcelable::class) {
readParcelable(T::class.java.classLoader, T::class.java)
} else {
readParcelable(Thread.currentThread().contextClassLoader, T::class.java)
}
} else {
readParcelable(T::class.java.classLoader)
}
}
The code for Serializable will be similar.
public inline fun <reified T> Parcel.readSerializable(): T? {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
if (T::class != Serializable::class) {
readSerializable(T::class.java.classLoader, T::class.java)
} else {
readSerializable(Thread.currentThread().contextClassLoader, T::class.java)
}
} else {
readSerializable() as T
}
}
These methods for reading and writing Serializable and Parcelable can also be used without payload, but for individual properties if you know for sure whether they are Serializable or Parcelable.
Can the Reverse Nesting Be Organized?
I considered an example where a Serializable object can be placed inside Parcelable. In my case, when there was a need to store an Intent object in Payload, this solution is the only one, since I have no ability to rewrite the Android sources and make Intent support Serializable, and the practical value of such a solution remains questionable anyway.
In the general case, implementing reverse nesting is possible: by adding the Serializable interface to your class, or by overriding serialization and deserialization, for example, by using of Externalizable (although the same custom serialization can also be done with Serializable, in Externalizable it looks more elegant).
Reflection
It always remains a mystery to me how everyday topics asked of junior developers in interviews can border so closely on non-obvious SDK specifics or require a deeper understanding of how the system works than even a senior developer may have.
How to Configure Kotlin Any Serialization with Parcelable and Serializable in Android was originally published in ProAndroidDev on Medium, where people are continuing the conversation by highlighting and responding to this story.




This Post Has 0 Comments