Android Parcelable: There's a better way
Introducing a new library that uses Kotlinx Serialization to parcellize objects on Android.
TL;DR
Introducing the new parcelable library which enables using kotlinx.serialization to serialize data into Android Parcels
to be passed between different Android Components.
Android Parcelable
Android's Parcelable is an object whose values can be written and read from a Parcel. An Android Parcel is a container of values that can be sent between different Android components, such as Activities and Fragments. So implementing the Parcelable interface allows an object to be passed between the different Android components.
The process of implementing the Parcelable interface has always been tedious, verbose, and error prone. It requires implementing the interface functions describeContents
and writeToParcel
, as well as, creating a static field called CREATOR
that implements the Parcelable.Creator
interface. This Parcelable.Creator
interface has two functions that need to be implemented: createFromParcel
and newArray
. And to make everything more complex, the order of operations matter; the contents of the class must be read in the same order they were written. Here's an example taken from the Parcelable documentation and converted to Kotlin:
class MyDataClass : Parcelable {
private val mData: Int
constructor(mData: Int) {
this.mData = mData
}
private constructor(parcel: Parcel) {
this.mData = parcel.readInt()
}
override fun describeContents(): Int = 0
override fun writeToParcel(out: Parcel, flags: Int) {
out.writeInt(mData)
}
companion object {
val CREATOR: Parcelable.Creator<MyParcelable> = object : Parcelable.Creator<MyParcelable> {
override fun createFromParcel(parcel: Parcel): MyParcelable = MyParcelable(parcel)
override fun newArray(size: Int): Array<MyParcelable?> = arrayOfNulls(size)
}
}
}
As you can see this requires a lot of code just to be able to pass a single object to a different Activity or component. There must be an easier way to "parcel" or "serialize" our objects...
Java Serializable
The Java Serializable interface is available on Android and provides a different means of serializing an object so that it can be passed between different components. This approach is much simpler than implementing Parcelable
because all that is necessary is to implement the Serializable
interface:
class MyDataClass(val mData: Int): Serializable
It's obvious this is much simpler than the Parcelable
approach, but it comes at a cost. As detailed in this StackOverflow answer, the Serializable
uses reflection to handle the serializing of the object and thus is much slower. On Android, where we are generally constrained on processing ability compared to computers, performance is of the upmost importance. So sacrificing performance for readability in this case may not be the best approach. Then let's take a look at another way to parcel objects on Android.
Kotlin Android Extensions
The Kotlin Android Extensions Gradle Plugin provides a way to parcel objects on Android using a @Parcelize
annotation. This Plugin (which has since been moved to it's own Plugin: kotlin-parcelize), greatly reduces the amount of code needed to be written in order to parcel an object by generating the parceling code for you. Simply annotate your model class with the @Parcelize
annotation and implement the Parcelable
interface, and you're all set!
@Parcelize
class MyDataClass(val mData: Int): Parcelable
This is an extremely simply approach and still uses the Parcelable
interface so it is much more performant at runtime than the Java Serializable
approach. The @Parcelize
annotation works for most usecases but what if we have a more complex object that requires specific serialization? In those scenarios, there is the Parceler
interface that allows you to manually serialize your object in an easier way than completely implementing the Parcelable
interface:
object MyDataClassParceler : Parceler<MyDataClass> {
override fun create(parcel: Parcel) = MyDataClass(parcel.readInt())
override fun MyDataClass.write(parcel: Parcel, flags: Int) {
parcel.writeInt(mData)
}
}
This approach allows us to simply parcel our objects and has the flexibility to handle more complex use cases. However, with the rise of popularity of Kotlin Multi-platform, model classes are typically created in common code, separating them from Android specific components, such as the @Parcelize
annotation. This would require us to create Parcelers
in the Android module for every class we wish to parcel from the common module, defeating the purpose of using the plugin in the first place.
moko-parcelize
There's a library by IceRock Development called moko-parcelize that brings the @Parcelize
functionality to Kotlin Multi-platform. So now we can parcel our model classes in a common module just as we did before with the Android class:
@Parcelize
class MyDataClass(val mData: Int): Parcelable
The lingering issue with the @Parcelize
approach is that for complex classes we still have to manually write Parcelers
, which serialize our models to a Parcel
. This helps serialize the models for use with Android components but it doesn't help serialize our models for other scenarios, such as formatting the model to JSON.
kotlinx.serialization
The kotlinx.serialization library provides a way to serialize/deserialize between Kotlin models and other formats. The library supports JSON, Protobuf, CBOR, Hocon, and Properties. And there are third party libraries that add extra formats, like the xmlutil library that adds XML support to Kotlinx Serialization.
Typically in a project, you would use kotlinx.serialization to handle serializing your Kotlin models to and from JSON for HTTP requests. For complex models you can create a custom KSerializer
. The problem here is that for complex models, we would have to create a custom KSerializer
and a custom Parceler
just to be able to serialize our models throughout the application. This redundancy is verbose, tedious, and error prone. So this lead me to an idea: what if there were custom Encoders and Decoders for the kotlinx.serialization library that handled parceling our models? This would remove the redundancy of multiple custom serializers.
chRyNaN/parcelable
I created the parcelable library as a companion to the kotlinx.serialization library. It provides custom Encoders
and Decoders
that handle writing and reading from an Android Parcel
. This means that @Serialiable
classes just work with Android components and any custom KSerializers
work as well.
Using the library is very straightforward:
- Create your
@Serializable
model (and any necessaryKSerializers
) using the kotlinx.serialization library:
@Serializable
data class MyDataClass(
mData: Int
)
- Create a
Parcelable
object or use the default:
val parcelable = Parcelable {
serializersModule = mySerializersModule
}
// Or
val parcelable = Parcelable.Default
- Then pass your model through
Intents
andBundles
just as you normally would but provide theparcelable
instance as an extra parameter:
// Put
intent.putExtra(key, myModel, parcelable)
bundle.putParcelable(key, myModel, parcelable)
// Get
val myModel = intent.getParcelableExtra(key, parcelable)
val myModel = bundle.getParcelable(key, parcelable)
And that's all there is to it! If you need custom serialization, you would create a custom KSerializer
for the kotlinx.serialization library, and as long as you don't use a specific encoder/decoder, it should work with Android's Parcel
. No duplicating serializing logic, no Android specific components, and no extra annotation processors.
Conclusion
There are numerous approaches to parceling data to pass between different Android Components. The simplest of these approaches is to use the kotlinx.serialization library alongside the new parcelable companion library. Together these libraries enable your models to be serialized from JSON HTTP Responses, to Parcels between Android Components.