- Type: Design proposal
- Status: Accepted
- Prototype: Implemented in Kotlin 1.1
The android.os.Parcelable
API requires substantial boilerplate for each parcelable class. In this document, we investigate the possible ways of mitigating this in Kotlin via a compiler extension.
The android.os.Parcelable
API requires some boilerplate code to be implemented (see here):
public class MyParcelable implements Parcelable {
private int mData;
public int describeContents() {
return 0;
}
public void writeToParcel(Parcel out, int flags) {
out.writeInt(mData);
}
public static final Parcelable.Creator<MyParcelable> CREATOR
= new Parcelable.Creator<MyParcelable>() {
public MyParcelable createFromParcel(Parcel in) {
return new MyParcelable(in);
}
public MyParcelable[] newArray(int size) {
return new MyParcelable[size];
}
};
private MyParcelable(Parcel in) {
mData = in.readInt();
}
}
For many simple cases, users want to do something like
@Parcelize
class MyParcelable(val data: Int): Parcelable
The name of the annotation is just an example here. Note: we can't use
Parcelable
for the annotation name as it clashes with the base interface name.
There's a number of annotation processing libraries that mitigate this:
To implement Parcelable
manually in Kotlin, one has to create a companion object and use @JvmField
:
data class MyParcelable(var data: Int): Parcelable {
override fun describeContents() = 1
override fun writeToParcel(dest: Parcel, flags: Int) {
dest.writeInt(data)
}
companion object {
@JvmField
val CREATOR = object : Parcelable.Creator<MyParcelable> {
override fun createFromParcel(source: Parcel): MyParcelable {
val data = source.readInt()
return MyParcelable(data)
}
override fun newArray(size: Int) = arrayOfNulls<MyParcelable>(size)
}
}
}
This creates even more nesting and requires even more details to be memorized. There's a community plugin for IntelliJ IDEA/Android Studio that generates this boilerplate. The [PaperParcel] library seems to work with Kotlin too, but requires manual creation of CREATOR
fields:
@PaperParcel
data class User(
val id: Long,
val firstName: String,
val lastName: String
) : PaperParcelable {
companion object {
@JvmField val CREATOR = PaperParcelUser.CREATOR
}
}
We can have a compiler extension to generate all the required boilerplate behind the scenes. This is similar to what (some) annotation processors do, but
- Java annotation processors can't alter Kotlin code,
- Normally, annotation processors can't add methods to existing classes (this is possible through a private API only, and kapt does not support such an API),
- A compiler extension can be potentially more flexible in terms of suppressing errors and providing syntactic means to the user.
A compiler extension can generate serialization/deserialization logic for all properties in a primary constructor of a class marked with a special annotation:
@Parcelize
class MyParcelable(val data: Int): Parcelable
Note that the class is not required to be a data class.
The following requirements apply here:
- Only non-
abstract
classes are supported@Parcelize
can't annotate aninterface
orannotation class
sealed
andenum
classes may be supported- In case of
sealed class
es, we should check if all concrete implementations have a@Parcelize
annotation
- In case of
object
s are forbidden for now- Objects can also be supported. We can serialize just the qualified name because we can't make the new instance of
object
anyway
- Objects can also be supported. We can serialize just the qualified name because we can't make the new instance of
- The class annotated with
@Parcelize
must implementParcelable
(directly or through a chain of supertypes)- Otherwise it's a compile-time error
- The primary constructor should be accessible (non-private)
- All parameters of the primary constructor must be properties (
val
/var
), otherwise they can not be (de)serialized- If there is a non-property parameter, it is a compile-time error
- Properties with initializers declared in the class body are also difficult to deserialize correctly: we'd have to generate an alternative constructor that does not execute initializers (including
init
blocks) at all and only uses the serialized data, but this has all the issues thatjava.io.Serializable
has wrt "magic" object creation.- Properties in the class body must be marked with a
@Transient
annotation, otherwise it's a compiler warning⚠️ Or error?
- The primary constructor of the class is invoked in
createFromParcel()
- Properties in the class body must be marked with a
- If some property types are not parcelable (i.e. do not implement
Parcelable
, are not supported by theParcel
interface, and are not customized (see below)), it's a compile-time error - The user is not allowed to manually override methods of
Parcelable
or create theCREATOR
field- It results in a compilation error
⚠️ Question: maybe overridingdescribeContents()
is OK?- Actually we can figure out is there any file descriptor serialized, and generate the appropriate
describeContents()
implementation.- It's not possible in all cases. Example:
class Foo(val a: Parcelable /* FileDescriptor inside it */)
- So we should allow user override it when needed
- It's not possible in all cases. Example:
- Actually we can figure out is there any file descriptor serialized, and generate the appropriate
The annotations for Parcelable
classes, transient fields, etc. should sit in a complimentary runtime library that ships together with the compiler extension.
Sealed class example:
sealed class Maybe<out T : Parcelable> : Parcelable {
override fun describeContents(): Int = 0
object None : Maybe<Nothing>(), Parcelable.Creator<None> {
override fun writeToParcel(dest: Parcel, flags: Int) {}
override fun newArray(size: Int) = arrayOfNulls<None>(size)
override fun createFromParcel(source: Parcel) = this
@JvmField val CREATOR = this
}
class Some<out T : Parcelable>(val value: T) : Maybe<T>() {
override fun writeToParcel(dest: Parcel, flags: Int) {
dest.writeParcelable(value, 0)
}
companion object {
@JvmField val CREATOR = // ...
}
}
}
🌀 Check interactions with inheritance by delegation.
Some syntactic options:
- We could allow the user to omit the supertype
Parcelable
, to be more DRY and have only the annotation to signify that the class is Parcelable, but this would be more challenging wrt the tooling support. - We could annotate the supertype itself to make it more local:
class MyParcelable(val data: Int): @Auto Parcelable
-
I think this is less desirable for readability since using this functionality (as directed by the @Auto annotation in this case) has implications for the primary constructor
- We can have
KotlinParcelable
marker interface instead- It could be a "marker" type alias
- The names
Parcelize
,Transient
andKotlinParcelable
are just placeholders for now
The generated serialization logic should take into account the following:
- support for collections (lists, maps), arrays and
SparseArray
s, including nested collections - support for
IBinder
/IInterface
- support for
enum class
es andobject
s - support for file descriptors wrt
describeContents()
The generated code should be efficient:
- If the property type is not-null, no nullability information is written
- For
List<String>
,String
elements are written directly (withoutwriteValue()
) - If the
Parcelable
property references to thefinal class
, it is written directly (withoutwriteParcelable()
)
⚠️ Discussion: There is an option to generate two constructors:
- one ordinary primary constructor,
- another one that takes
Parcel
and creates an object from it.This has a benefit of being more convenient when it comes to hierarchies (see below). The issue here is that the second constructor will have to bypass any initializers in the class body or do a sophisticated combination of that logic and the deserialization. Overall it seems easier to have only one traditional constructor and call it from the
createFromParcel
method.
Sometimes users need to write their own custom serialization logic manually, but the boilerplate described above is undesirable. We can simplify this task by introducing the following convention:
@Parcelize
class MyParcelable(val data: Int) : Parcelable {
companion object : Parceler<MyParcelable> {
override fun MyParcelable.writeToParcel(parcel: Parcel, flags: Int) {
parcel.writeInt(data)
}
override fun createFromParcel(parcel: Parcel): MyParcelable {
return MyParcelable(parcel.readInt())
}
}
}
The Parceler
interface is defined in the same runtime library that comes with the compiler extension. It's defined as follows:
interface Parceler<P : Parcelable> {
fun describeContents() = 0
fun P.write(parcel: Parcel, flags: Int)
fun create(parcel: Parcel): P
fun newArray(size: Int): Array<P>
}
If a @Parcelize
class has a companion object that implements Parceler
:
- the necessary boilerplate is generated for the
Parcelable
convention and delegated to the companion object - the
newArray()
method of the companion object is implemented automatically unless it's explicitly overridden - the type argument to
Parceler
must match the containing class, otherwise it's a compile-time error
Note: Indirect implementations (object : Foo
, where Foo : Parceler
) are questionable here, but we can allow them if there are use cases for it.
Syntactic options:
- It does not have to be a companion object, a named object, e.g.
object Parceler: Parceler<MyParcelable>
may be ok too⚠️ It's rather better not to haveParceler
as a companion object becausenewArray
will be supported otherwise- But then we have to choose two different names for the supertype and for the actual object, as we can't write
object Parceler : Parceler
without qualified names - Also the
companion object
may be private
- We may want to require the object to be annotated to explicitly show that the
newArray()
is auto-generated
⚠️ Discussion: why not implementnewArray()
in theParceler
interface itself? First, this method cannot be implemented generically (with erasure), because the runtime type of the array created is different every time. We could do something likefun newArray(size: Int) = throw UnsupportedOperationException("This method must be overridden by subclasses")
, this has the benefit of making the IDE's life easier: otherwise we'd have to teach it to skipnewArray()
when generating stubs for the Override/Implement action in annotated classes. OTOH, this is error-prone in the case of non-annotated classes. A solution here could be to magically implementnewArray()
in all concrete subclasses ofParceler
regardless of the annotation. I wonder if this can be promoted to a general feature of Kotlin...
Existing annotation processors allow for per-field and per-type customization of serialization logic, e.g.:
@Parcelize
class MyParcelable(
@CustomParceler(FooParceler::class)
val foo: Foo,
@CustomParceler(Bar.Parceler::class)
val bar: Bar
): Parcelable
The @CustomParceler
annotation can be defined as follows:
annotation class CustomParceler(val parcelerClass: KClass<out Parceler<*>>)
Additional rules:
- the class passed as a custom Parceler for a property of type
T
must implementParceler<T>
and be a singleton (declared as object)
describeContents()
here? Should we bitwise-or all the parcelers? How do annotation processors go about it?
It would also be desirable to provide bulk customization for all properties of the same type, e.g. java.util.Date
. This can be done globally, e.g. through a meta-annotation, or locally, e.g.:
@Parcelize
@CustomParcelerForType(
type = Date::class,
parceler = DateParceler::class
)
class MyParcelable(val from: Date, val to: Date): Parcelable
Local customization is more flexible, but will likely result in a lot of duplication. Global customization is problematic wrt incremental and separate compilation, but this can be addressed in the future.
Hierarchies of Parcelable classes are challenging from two points of view:
- organizing constructors (see above)
- deserializing an instance of the right class from a Parcel, when the container has only a reference to a superclass.
The latter issue seems to be resolvable by "tagging" such records through writing fully-qualified names of concrete classes to parcels, but it's not clear whether it's a good idea, since
- it involves reflection
- it will increase the size of parcels
This proposal does not support hierarchies for now.