A study of the Parcelize feature from Kotlin Android Extensions

Life is too short to waste time on writing Parcelable code

  • It’s an official plugin made by JetBrains with the collaboration of Google and is guaranteed to be well supported in the future
  • The generated code of @Parcelize is very efficient (as we’ll discover further in this article)
  • The CREATOR field doesn’t have to be declared at all, along with that easy-to-forget @JvmField annotation
  • Thanks to the Parceler interface, it is possible to write simple plugins to handle unsupported types or override the default implementation
  • No extra classes are created by the plugin. All the generated code is embedded in the annotated class so the app will behave exactly as if you wrote the code yourself
  • There is no runtime library overhead and zero extra methods are added to your app.

Project setup

First, you should upgrade your Kotlin Gradle plugins and Android Studio plugin to version 1.3.60 or more recent.

For Kotlin 1.3.60 to 1.4.10

To enable the “Parcelable implementation generator” feature, you have to enable the Kotlin Android Extensions Gradle plugin in your project by simply declaring it at the top of your module’s build.gradle file, just after the kotlin-android plugin:

apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
androidExtensions {
features = ["parcelize"]
}

Since Kotlin 1.4.20

All other features of the Kotlin Android Extensions Gradle plugin have been deprecated since Kotlin 1.4.20 and Parcelize has moved to its own Gradle plugin. Enable it by declaring it at the top of your module’s build.gradle file, just after the kotlin-android plugin:

apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-parcelize'

Usage

Just add the @Parcelize annotation to a class implementing the Parcelable interface and the Parcelable implementation will be generated automatically.

@Parcelize
class Person(val name: String, val age: Int) : Parcelable

Analyzing the generated code

This looks fine so far, but what about the quality of the generated code? Is the implementation as efficient as it could be? Let’s find out.

Basic types

@Parcelize
class Basic(val aByte: Byte, val aShort: Short,
val anInt: Int, val aLong: Long,
val aFloat: Float, val aDouble: Double,
val aBoolean: Boolean, val aString: String) : Parcelable
public void writeToParcel(@NotNull Parcel parcel, int flags) {
parcel.writeByte(this.aByte);
parcel.writeInt(this.aShort);
parcel.writeInt(this.anInt);
parcel.writeLong(this.aLong);
parcel.writeFloat(this.aFloat);
parcel.writeDouble(this.aDouble);
parcel.writeInt(this.aBoolean);
parcel.writeString(this.aString);
}
public static class Creator implements android.os.Parcelable.Creator {
@NotNull
public final Object[] newArray(int size) {
return new Basic[size];
}

@NotNull
public final Object createFromParcel(@NotNull Parcel in) {
return new Basic(in.readByte(), (short)in.readInt(), in.readInt(), in.readLong(), in.readFloat(), in.readDouble(), in.readInt() != 0, in.readString());
}
}

Nullable types

Here is another Kotlin sample with various types of nullable fields.

@Parcelize
class NullableFields(
val aNullableInt: Int?,
val aNullableFloat: Float?,
val aNullableBoolean: Boolean?,
val aNullableString: String?
) : Parcelable
public void writeToParcel(@NotNull Parcel parcel, int flags) {
Integer var10001 = this.aNullableInt;
if (var10001 != null) {
parcel.writeInt(1);
parcel.writeInt(var10001);
} else {
parcel.writeInt(0);
}

Float var3 = this.aNullableFloat;
if (var3 != null) {
parcel.writeInt(1);
parcel.writeFloat(var3);
} else {
parcel.writeInt(0);
}

Boolean var4 = this.aNullableBoolean;
if (var4 != null) {
parcel.writeInt(1);
parcel.writeInt(var4);
} else {
parcel.writeInt(0);
}

parcel.writeString(this.aNullableString);
}

Nested Parcelables

Let’s declare a Parcelable class embedding a Parcelable field.

@Parcelize
class Book(val title: String, val author: Person) : Parcelable
public void writeToParcel(@NotNull Parcel parcel, int flags) {
parcel.writeString(this.title);
this.author.writeToParcel(parcel, 0);
}
@NotNull
public final Object createFromParcel(@NotNull Parcel in) {
return new Book(in.readString(), (Person)Person.CREATOR.createFromParcel(in));
}

Enum classes

enum classes are supported out-of-the-box by the plugin.

enum class State {
ON, OFF
}

@Parcelize
class PowerSwitch(var state: State) : Parcelable
public void writeToParcel(@NotNull Parcel parcel, int flags) {
parcel.writeString(this.state.name());
}

Bonus feature: make enum classes implement Parcelable

You can actually annotate an enum class itself if you make it implement Parcelable.

@Parcelize
enum class State : Parcelable {
ON, OFF
}
val args = Bundle(1).apply {
putParcelable("state", state)
}
val args = bundleOf("state" to state)

Collections

@Parcelize supports a wide range of collections by default:

  • All array types (except ShortArray)
  • List, Set, Map interfaces (mapped to ArrayList, LinkedHashSet and LinkedHashMap respectively)
  • ArrayList, LinkedList, SortedSet, NavigableSet, HashSet, LinkedHashSet, TreeSet, SortedMap, NavigableMap, HashMap, LinkedHashMap, TreeMap, ConcurrentHashMap
  • Android-specific framework collections: SparseArray, SparseIntArray, SparseLongArray, SparseBooleanArray.
@Parcelize
class Library(val books: List<Book>) : Parcelable
public void writeToParcel(@NotNull Parcel parcel, int flags) {
List var10002 = this.books;
parcel.writeInt(var10002.size());
Iterator var10000 = var10002.iterator();

while(var10000.hasNext()) {
((Book)var10000.next()).writeToParcel(parcel, 0);
}
}
  • It allows Parcelize to support more collection types than the ones provided by the Parcel collections API, without adding extra methods to the code.
  • Serialization will be more efficient for many types compared to the Parcel collections API implementation because the compiler plugin will always use the best serialization method for each value type in the collection. For example, serializing a SparseArray<Book> (where Book is a final class) using Parcel.writeSparseArray() will result in Parcel.writeValue() to be used to serialize the Book type. This method is less efficient because for each value it will write an extra integer describing the value type (which is unnecessary because all values are of the same type in this case), then use Parcel.writeParcelable() which is also less efficient than a nested call to Book.writeToParcel() as we saw earlier. In comparison, the Parcelize plugin will just generate a call to Book.writeToParcel() for each value.

Custom types serializers

The Parcelize plugin also provides support for the remaining types that are part of the Parcel API: CharSequence, Exception (which only supports a few types of Exceptions), Size, SizeF, Bundle, IBinder, IInterface and FileDescriptor.

object DateParceler : Parceler<Date> {

override fun create(parcel: Parcel) = Date(parcel.readLong())

override fun Date.write(parcel: Parcel, flags: Int)
= parcel.writeLong(time)
}
  • Annotating the Parcelable class allows to group annotations together and avoid repetition, especially if you have many properties of the same type for which you want a custom serializer.
@Parcelize
@TypeParceler<Date, DateParceler>
class Session(val title: String,
val startTime: Date,
val endTime: Date): Parcelable
  • Annotating the property type avoids any ambiguity regarding which implementation is going to be used to serialize a property. If there is a mismatch between the annotation and the type, a Lint warning will be shown in the IDE (but it won’t prevent compilation).
@Parcelize
class Session(
val title: String,
val startTime: @WriteWith<DateParceler> Date,
val endTime: @WriteWith<DateParceler> Date
) : Parcelable
public void writeToParcel(@NotNull Parcel parcel, int flags) {
parcel.writeString(this.title);
DateParceler.INSTANCE.write(this.startTime, parcel, flags);
DateParceler.INSTANCE.write(this.endTime, parcel, flags);
}

Handling nullable custom types

Even though Parcelize provides support for serializing the nullable variant of all built-in types, it doesn’t add automatic support for the nullable variant of a custom type based on its non-null custom type Parceler. This means that if you provide a Parceler implementation for the Date type, it won’t support Date? properties automatically unless you add another annotation for the Date? type as well.

@Parcelize
@TypeParceler<Date, DateParceler>
class Session(val title: String,
val startTime: Date,
val endTime: Date?): Parcelable
public void writeToParcel(@NotNull Parcel parcel, int flags) {
parcel.writeString(this.title);
DateParceler.INSTANCE.write(this.startTime, parcel, flags);
parcel.writeSerializable(this.endTime);
}
@Parcelize
@TypeParceler<Date, DateParceler>
@TypeParceler<Date?, DateParceler>
class Session(val title: String,
val startTime: Date,
val endTime: Date?): Parcelable
public void writeToParcel(@NotNull Parcel parcel, int flags) {
parcel.writeString(this.title);
DateParceler.INSTANCE.write(this.startTime, parcel, flags);
DateParceler.INSTANCE.write(this.endTime, parcel, flags);
}

Handling generic custom types

One limitation of custom Parceler implementations is that they are required to be object singletons. Which means they can’t be passed extra arguments through a constructor: a separate object needs to be declared for each variation of the serialization algorithm.

open class EnumSetParceler<E : Enum<E>>(private val elementType: Class<E>) : Parceler<EnumSet<E>> {

private val values = elementType.enumConstants

override fun create(parcel: Parcel): EnumSet<E> {
val set = EnumSet.noneOf(elementType)
for (i in 0 until parcel.readInt()) {
set.add(values[parcel.readInt()])
}
return set
}

override fun EnumSet<E>.write(parcel: Parcel, flags: Int) {
parcel.writeInt(size)
for (value in this) {
parcel.writeInt(value.ordinal)
}
}
}
object StateEnumSetParceler
: EnumSetParceler<State>(State::class.java)
object CategoryEnumSetParceler
: EnumSetParceler<Category>(Category::class.java)

Accessing generated CREATOR fields from Kotlin code

When writing custom Parcel deserialization code, for example by implementing a Parceler object, sometimes you need to reference the static CREATOR field of Parcelable classes. It’s useful to create an object instance for a well-known type without relying on reflection, and is used among others by Parcel.createTypedArrayList() and Parcel.readTypedObject().

  • Manually write the Parcelable implementation for those specific classes for which you need to access the CREATOR field, instead of having them generated automatically.
  • In case of custom collection types, use standard collection types which are supported out-of-the-box instead.
  • Use the less efficient Parcel methods which resolve the CREATOR fields using reflection: Parcel.readArrayList() in place of Parcel.createTypedArrayList() and Parcel.readParcelable() in place of Parcel.readTypedObject(). The serialized data format of these methods is less compact because the class name is written before each entry. The CREATOR fields are cached by the Parcel instance after the first reflective lookup so the performance impact should be minimal.
  • Rely on Java cross-compilation: the generated CREATOR field can actually be retrieved from a Java class, which can in turn expose it back to Kotlin code. Mixing Kotlin and Java in the same module will make the compilation a bit slower compared to a Kotlin-only module and creating a bridge Java class definitely feels like an ugly hack, but this will allow you to write the most efficient custom type serializers while still relying on generated code.
Sokoban puzzle by Borgar Þorsteinsson. Under Creative Commons license

Dealing with class inheritance

You can annotate open classes with @Parcelize. However, it’s not possible to also annotate child classes that inherit from these open classes without duplicating all the serializable properties of their parent class. Let’s look at an example to have a better understanding of the problem:

@Parcelize
open class Book(val title: String, val author: Person) : Parcelable

@Parcelize
class ElectronicBook(private val _title: String,
private val _author: Person,
val downloadSize: Long) : Book(_title, _author)

Parcelable abstract and sealed classes with common fields

It’s not allowed to annotate abstract or sealed classes with @Parcelize: all concrete child classes that inherit from them must be annotated instead.

  • It’s not possible to reuse serialization code from a parent class in a child class, meaning that the generated code needed to serialize a field declared in the parent class will effectively be repeated in every child class;
  • All properties that need to be serialized in the abstract or sealed class must be declared as abstract in order to avoid duplicating them in the child class.
abstract class Vehicle(val wheels: Int) : Parcelable {
abstract val model: String
abstract var speed: Float
}

@Parcelize
class Car(override val model: String, override var speed: Float)
: Vehicle(4)

@Parcelize
class Bicycle(override val model: String, override var speed: Float)
: Vehicle(2)
  • The common wheels property should not be serialized and its value is initialized directly by the child classes constructor.
  • The common model and speed properties need to be serialized in each instance, so they are declared as abstract in the parent Vehicle class. Then, they are overridden in the primary constructor of all child classes and will be handled by Parcelize.
  • Extra properties to serialize could also be added in the primary constructor of the child classes.

Objects with a parent class

Another interesting feature of Parcelize that has not yet been mentioned is that it supports annotating object. At first it would seem useless to support the serialization of a singleton, but it actually makes sense when the object has a parent class and represents one of the possible values of its parent class.

sealed class Status : Parcelable

@Parcelize
object Success : Status()

@Parcelize
class Error(val message: String) : Status()
@NotNull
public final Object createFromParcel(@NotNull Parcel in) {
return in.readInt() != 0 ? Success.INSTANCE : null;
}

Excluding properties from serialization

Sometimes properties are used to store transient state that must be excluded from serialization. In a typical Java world, the containing class would implement the Serializable interface and the excluded fields would be annotated with @Transient.

@Parcelize
class Book(val title: String, val totalPages: Int) : Parcelable {
@IgnoredOnParcel
var readPages: Int = 0
}
@Parcelize
class Book private constructor(val title: String,
val totalPages: Int) : Parcelable {
@IgnoredOnParcel
var readPages: Int = 0

constructor(title: String, totalPages: Int, readPages: Int)
: this(title, totalPages) {
this.readPages = readPages
}
}
@Parcelize
class ClickCounter private constructor(private var count: Int)
: Parcelable {

constructor() : this(0)

fun click() {
count++
}

val currentValue
get
() = count
}

--

--

Android developer from Belgium, blogging about advanced programming topics.

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store
Christophe Beyls

Android developer from Belgium, blogging about advanced programming topics.