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.