Use Parceler to put your parcels on a diet

kotlin-parcelize is a great tool. Its simple to use and it helps in avoiding writing a lot of boilerplate code. There are times though that we need to take control of writing and reading to/from the parcel. One of these times is to cut down a few bytes from it (TransactionTooLargeException I am looking at you).

Meet me in the middle

@Parcelize takes full control and creates everything. Without the annotation, the developer has to do this on her own. Parceler lives in the middle of this spectrum. The plugin will create all necessary methods and classes but the actual write and read to/from the parcel will be the developer’s responsibility.

Without a Parceler the write/read looks like this:

public void writeToParcel(@NotNull Parcel parcel, int flags) {
Intrinsics.checkNotNullParameter(parcel, "parcel");
parcel.writeInt(this.id);
parcel.writeString(this.description);
parcel.writeString(this.priority.name());
parcel.writeParcelable(this.status, flags);
Attachment var10001 = this.attachment;
if (var10001 != null) {
parcel.writeInt(1);
var10001.writeToParcel(parcel, 0);
} else {
parcel.writeInt(0);
}
}
@NotNull
public final Task createFromParcel(@NotNull Parcel in) {
Intrinsics.checkNotNullParameter(in, "in");
return new Task(
in.readInt(),
in.readString(),
(Priority)Enum.valueOf(Priority.class, in.readString()),
(Status)in.readParcelable(Task.class.getClassLoader()),
in.readInt() != 0 ? (Attachment)Attachment.CREATOR.createFromParcel(in) : null
);
}

with a Parceler like this (where the Companion object is acting as a Parceler):

public void writeToParcel(@NotNull Parcel parcel, int flags) {
Intrinsics.checkNotNullParameter(parcel, "parcel");
Companion.write(this, parcel, flags);
}
@NotNull
public final Task createFromParcel(@NotNull Parcel in) {
Intrinsics.checkNotNullParameter(in, "in");
return Task.Companion.create(in);
}

Cutting down parcel’s size

The above-generated code is based on Task

@Parcelize
class Task(
val id: Int,
val description: Description,
val priority: Priority = Normal,
val status: Status = NotStarted,
val attachment: Attachment? = null
) : Parcelable
@Parcelize
class Attachment(val path: String) : Parcelable
@Parcelize
@JvmInline
value class Description(val value: String) : Parcelable
enum class Priority {
Low,
Normal,
High
}
sealed class Status : Parcelable {
@Parcelize
object NotStarted : Status()
@Parcelize
object InProgress : Status()
@Parcelize
class Completed(val completedAt: LocalDate) : Status()
}

which, creates a parcel of 248 bytes. The code does not do anything weird. All primitives, which include the value classes too, are well handled. So nothing to do here. This leaves parcelables and enums.

But first, let’s use a Parceler. This means that writing and reading to/from the parcel has to be implemented by us. For starters, we will do exactly what the generated code does except for the attachment property. For that, the generated code uses parcelable’s methods and CREATOR. In the Parceler we don’t have access to the CREATOR.

companion object : Parceler<Task> {
override fun create(parcel: Parcel): Task {
return Task(
parcel.readInt(),
Description(parcel.readString()!!),
Priority.valueOf(parcel.readString()!!),
parcel.readParcelable(Status::class.java.classLoader)!!,
parcel.readParcelable(Attachment::class.java.classLoader)
)
}
override fun Task.write(parcel: Parcel, flags: Int) {
with(parcel) {
writeInt(id)
writeString(description.value)
writeString(priority.name)
writeParcelable(status, flags)
writeParcelable(attachment, flags)
}
}
}

That leaves us with writeParcelable and readParcelable but now the parcel’s size is bigger, it is 328 bytes! Turns out that writeParcelable first writes the parcelable’s name and then the parcelable itself!

We need to use the CREATOR. After searching around I found parcelableCreator. A function that solved a well-known problem and will be added to Kotlin 1.6.20.

inline fun <reified T : Parcelable> Parcel.readParcelable(): T? {
val exists = readInt() == 1
if (!exists) return null
return parcelableCreator<T>().createFromParcel(this)
}
@Suppress("UNCHECKED_CAST")
inline fun <reified T : Parcelable> parcelableCreator(): Parcelable.Creator<T> =
T::class.java.getDeclaredField("CREATOR").get(null) as? Parcelable.Creator<T>
?: throw IllegalArgumentException("Could not access CREATOR field in class ${T::class.simpleName}")
fun <T : Parcelable> Parcel.writeParcelable(t: T?) {
if (t == null) {
writeInt(0)
} else {
writeInt(1)
t.writeToParcel(this, 0)
}
}

This allows us to revert the size increment back to 248 bytes

companion object : Parceler<Task> {
override fun create(parcel: Parcel): Task {
return Task(
//…
parcel.readParcelable()
)
}
override fun Task.write(parcel: Parcel, flags: Int) {
with(parcel) {
//…
writeParcelable(attachment)
}
}
}

Use enum’s ordinal than its name. The generated code writes enum’s name so that it can use Enum.valueOf when reading. We can write an int instead by using enum’s ordinal

companion object : Parceler<Task> {
override fun create(parcel: Parcel): Task {
return Task(
//…
parcel.readEnum()
)
}
override fun Task.write(parcel: Parcel, flags: Int) {
with(parcel) {
//…
writeEnum(priority)
}
}
}
inline fun <reified T : Enum<T>> Parcel.readEnum(): T {
return enumValues<T>()[readInt()]
}
inline fun <reified T : Enum<T>> Parcel.writeEnum(t: T) {
writeInt(t.ordinal)
}

and use Enum.values() when reading. This drops the parcel’s size to 232 bytes.

Skip a class’s parcelable implementation. This of course depends on each implementation.
For instance, Status is a sealed class that only one of its children has a construction parameter. We can leverage this by writing only that value

companion object : Parceler<Task> {
override fun create(parcel: Parcel): Task {
return Task(
//…
parcel.readStatus()
)
}
override fun Task.write(parcel: Parcel, flags: Int) {
with(parcel) {
//…
writeStatus(status)
}
}
}
fun Parcel.readStatus(): Status {
return readLong().let { value ->
when (value) {
0L -> NotStarted
1L -> InProgress
else -> Completed(LocalDate.ofEpochDay(value))
}
}
}
fun Parcel.writeStatus(status: Status) {
when (status) {
is Completed -> writeLong(status.completedAt.toEpochDay())
InProgress -> writeLong(1)
NotStarted -> writeLong(0)
}
}

this drops the parcel’s size to 136 bytes!

Conclusion

Fortunately, the generated code does a pretty good job and making any optimizations is not that common. But when needed Parceler and parcelableCreator are great tools.

PS: for measuring the parcel’s size I was using this method

fun Parcelable.sizeInBytes(): Int {
val parcel = Parcel.obtain()
try {
parcel.writeParcelable(this, 0)
return parcel.dataSize()
} finally {
parcel.recycle()
}
}

which was shamelessly stolen from Guardian’s TooLargeTool.