Project setup

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

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:

apply plugin: 'com.android.application'

apply plugin: 'kotlin-android'

apply plugin: 'kotlin-android-extensions'

By using the newer version of the plugin, it’s not necessary anymore to turn on experimental mode to be able to use the @Parcelize feature or for the IDE to detect it as available.

However, I’m not a big fan of the other features provided by this plugin like the View binding and caching. By default, this will scan your layouts and generate static properties for every View with an id, as well as adding a hidden View cache in your Activities and Fragments. I don’t want it. Fortunately, there is a way to enable Parcelize while disabling other features of the plugin. Add this extra section to the same build.gradle file, below the android section:

androidExtensions {

features = ["parcelize"]

}

Now we’re all set to use the @Parcelize annotation.

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

This is the same example as in the previous article, in just 2 lines of code.

The class can be a data class but it’s optional. All properties to serialize have to be declared in the primary constructor, and like a data class all parameters of the primary constructor need to be properties. They can be val or var , private or public.

The main limitation of the feature is that it is not allowed to annotate abstract or sealed classes. To make them implement the Parcelable interface you can annotate each of their child classes instead. We’ll go further into details with concrete examples in a following chapter.

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.

We’re going to use the Kotlin Bytecode inspector on the annotated class and decompile it back to Java to reveal the code under the hood.

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

The following code will be generated:

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);

}

For basic types like primitives and String , we see that the proper dedicated methods from the Parcel class are used as expected. Notice that the Boolean value is written as an Int . This is because at the Java bytecode level, a boolean is represented as a primitive integer whose value is either 0 or 1. The programming language hides this abstraction. Also, the Short value is written as an Int as well, because the Parcel API doesn’t provide a way to serialize Short values so the next bigger integer type is used instead.

The code which reads back the values looks like this:

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());

}

}

Besides the above code, no additional constructor or method is added to the base class and all of the Parcelable deserialization code fits inside the createFromParcel() method of the Creator class. This guarantees that the number of methods in the application’s APK file will be as low as possible, which is always important.

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

The generated serialization code decompiles to this:

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);

}

For nullable counterparts of primitive types, the plugin generates code which writes an extra integer before the value to indicate if it’s null or not, which is just as efficient as a manual implementation. As for the String? value, Parcel.writeString() already properly handles null values so no integer is added to serialize this type.

Nullable fields are handled properly and efficiently for every type.

Nested Parcelables

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

@Parcelize

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

Person is the class we defined earlier, which is also Parcelable . Here is the representation of the serialization code:

public void writeToParcel(@NotNull Parcel parcel, int flags) {

parcel.writeString(this.title);

this.author.writeToParcel(parcel, 0);

}

Interesting: the plugin generated a direct nested call to writeToParcel() . This is equivalent to calling Parcel.writeTypedObject() . It’s very efficient because it allows skipping writing the class name of the nested type like Parcel.writeParcelable() does. This information is normally used during deserialization in order to perform a small reflection lookup to retrieve the CREATOR field corresponding to the correct subtype.

But in this case the CREATOR field to use is well-known and unique, because the Person class is final . That’s why the deserialization code looks like this:

@NotNull

public final Object createFromParcel(@NotNull Parcel in) {

return new Book(in.readString(), (Person)Person.CREATOR.createFromParcel(in));

}

For non-final classes or interfaces, the Parcelize plugin will generate a call to Parcel.writeParcelable() instead.

Classes are final by default in Kotlin, allowing optimized nested serialization to be used in most cases. If you are including a Java Parcelable class as a field of a Kotlin class annotated with @Parcelize , declare the class as final whenever possible to be able to benefit from the same optimization.

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

Serialization code:

public void writeToParcel(@NotNull Parcel parcel, int flags) {

parcel.writeString(this.state.name());

}

The enum value name is written as a String and the deserialization code uses Enum.valueOf(Class enumType, String name) to transform the name back to an enum value. This is definitely more efficient than using writeSerializable() which must be avoided at all costs on Android for performance and efficiency reasons. Internally, the Enum implementation uses a name cache to quickly retrieve an enum value from its name.

Another way of efficiently serializing an enum value would be to store its ordinal() integer, with the downside of having the values array cloned each time EnumType.values() is called during deserialization. In summary, the code generated by the plugin is fine in terms of performance.

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

}

Unlike regular classes whose properties are serialized, the enum classes have their value name serialized so that the proper value instance is retrieved from memory on deserialization (since enum values are singletons).

This is particularly handy when you want to put an enum value in a Bundle (as an Intent extra or Fragment argument for example), without having to use Bundle.putSerializable() which is slower and less efficient and without having to write your own helper methods for custom serialization. Now you can just write:

val args = Bundle(1).apply {

putParcelable("state", state)

}

Or even simpler, you can use the bundleOf() factory function from the core-ktx library and the argument will be serialized as a Parcelable automatically, because the Parcelable interface has a higher priority than Serializable in the function.

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)

, , interfaces (mapped to , and respectively) ArrayList , LinkedList , SortedSet , NavigableSet , HashSet , LinkedHashSet , TreeSet , SortedMap , NavigableMap , HashMap , LinkedHashMap , TreeMap , ConcurrentHashMap

, , , , , , , , , , , , Android-specific framework collections: SparseArray , SparseIntArray , SparseLongArray , SparseBooleanArray .

For the following types: ByteArray , IntArray , CharArray , LongArray , FloatArray , DoubleArray , BooleanArray , Array<String> , List<String> , Array<Binder> , the generated code will simply call the dedicated optimized methods from the Parcel API. For other array and collection types, things work differently.

Here is an example of a class including a collection of objects implementing Parcelable .

@Parcelize

class Library(val books: List<Book>) : Parcelable

The bytecode inspector reveals that the generated code doesn’t use Parcel.writeTypedList() from the Parcel API but instead inlines the logic to handle the list directly in the body of the writeToParcel() method:

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);

}

}

Same goes for the deserialization code. This way of doing things actually has two benefits:

It allows Parcelize to support more collection types than the ones provided by the Parcel collections API, without adding extra methods to the code.

than the ones provided by the 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.

Note that ArrayMap , ArraySet , LongSparseArray from the Android framework are not supported by default, as well as all the unbundled collection classes provided by the Jetpack libraries: ArrayMap , ArraySet , LongSparseArray , SimpleArrayMap , SparseArrayCompat . Consider using SparseArray instead of SparseArrayCompat , and write your own custom serializer for the other types if you need them.

Finally, ArrayDeque , EnumMap and EnumSet may look like they are supported, but it’s only because they implement the Serializable interface and the generated code will just fall back to using Parcel.writeSerializable() which is slow and inefficient. Therefore it’s highly recommended to either use more generic collection types like Set<Enum> or write your own custom serializer, as we’ll see in the next chapter.