Android Parcelable: There's a better way

Introducing a new library that uses Kotlinx Serialization to parcellize objects on Android.

Android Parcelable: There's a better way

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 necessary KSerializers) 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 and Bundles just as you normally would but provide the parcelable 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.