A study of the Parcelize feature from Kotlin Android Extensions

  • 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

For Kotlin 1.3.60 to 1.4.10

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

Since Kotlin 1.4.20

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

Usage

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

Analyzing the generated code

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

@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

@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 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

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

Collections

  • 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

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

@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

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

  • 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

@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 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

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

@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.

Love podcasts or audiobooks? Learn on the go with our new app.

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