3

Use Parceler to put your parcels on a diet – le0nidas

 2 years ago
source link: https://le0nidas.gr/2022/03/20/use-parceler-to-put-your-parcels-on-a-diet/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

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.

Loading...

Related

Lets build a coroutineFebruary 23, 2021In "Kotlin"

The memento design pattern in KotlinJanuary 7, 2021In "Design Patterns"

Introduction to GitHub ActionsAugust 29, 2020In "Introduction"


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK