A Parcel
is an optimised serialisation format for Android which is designed to enable us to transfer data between processes. This is something that most Android developers need to do occasionally, but not that often. Making a class Pareclizable
actually requires a little bit of effort, but there’s a Kotlin extension that simplifies things enormously. In this post we’ll look at @Parcelize
and how it can make our lives easier.
I am sure that I’m not alone when every time I need to implement a Parcelable
I slightly despair because I know that I need to write some boilerplate code, and I’ll need to look up the docs to remind me how to do it because it is something that I don’t do regularly. Kotlin to the rescue!
Typically the kind of data that we need to pass between processes can be encapsulated within a data class. If the data class implements Parcelable
we typically need to include a few methods:
data class SimpleDataClass( val name: String, val age: Int ) : Parcelable { constructor(parcel: Parcel) : this( parcel.readString()!!, parcel.readInt() ) override fun writeToParcel(parcel: Parcel, flags: Int) { parcel.writeString(name) parcel.writeInt(age) } override fun describeContents(): Int { return 0 } companion object CREATOR : Parcelable.Creator{ override fun createFromParcel(parcel: Parcel): SimpleDataClass { return SimpleDataClass(parcel) } override fun newArray(size: Int): Array { return arrayOfNulls(size) } } }
Thankfully, IntelliJ IDEA and Android Studio contain helpers to generate this code automatically, but it is still adding cruft to our data class. The @Parcelize
annotation is our friend! It is still in experimental state, but has been around for a little while so we can hope that it becomes stable soon. Many thanks to Olivier Genez for pointing out that @Parcelize
made it out of experimental in Kotlin 1.3.40 (this article was originally written before that was released). To use @Parcelize
in a version of Kotlin prior to 1.3.40 we need to turn on experimental Android extensions:
. . . androidExtensions { experimental = true } . . .
With @Parcelize
can now simplify our data class quite enormously:
@Parcelize data class SimpleDataClass( val name: String, val age: Int ) : Parcelable
While this might appear to be the shortest post in Styling Android history, it’s not quite that simple. Let’s look at a slightly more complex example:
@Parcelize data class CompoundDataClass @JvmOverloads constructor( val name: String, val simpleDataClass: SimpleDataClass, @Transient val transientString: String = "" ) : Parcelable
If you like a puzzle, then please study that snippet and see if you can work out what doesn’t quite work as expected.
At first glance one might consider that including a field of type SimpleDataClass
might cause a problem, but this is actually fine because it is already a Parcelable
and we can use any Parcelable
class as a field. The issue is actually with the transientString
field, and it’s not quite a simple as it seems. Reading the code, one would understandably assume that the receiver of this would get an empty string in the transientString
field, but this doesn’t happen. The problem is actually twofold: Firstly, the Android framework only serialises a Parcelable
when it has to; And secondly, @Parcelize
doesn’t respect the @Transient
annotation.
To demonstrate the first of these, look at the following Fragment
:
class MainFragment : Fragment() { private var simple: SimpleDataClass? = null private var compound: CompoundDataClass? = null override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) arguments?.let { simple = it.getParcelable(ARG_SIMPLE) compound = it.getParcelable(ARG_COMPOUND) } Timber.d("Simple: \"%s\"; Compound: \"%s\"", simple, compound) } override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View? = inflater.inflate(R.layout.fragment_main, container, false) companion object { private const val ARG_SIMPLE = "simple" private const val ARG_COMPOUND = "compound" @JvmStatic fun newInstance(simpleDataClass: SimpleDataClass, compound: CompoundDataClass) = MainFragment().apply { arguments = Bundle().apply { putParcelable(ARG_SIMPLE, simpleDataClass) putParcelable(ARG_COMPOUND, compound) } } } }
if we call this as follows we might expect different behaviour than actually happens:
class MainActivity : AppCompatActivity() { private val simple = SimpleDataClass("Simple", 1) private val compound = CompoundDataClass("Compound", simple, "Transient") override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) supportFragmentManager.beginTransaction().apply { replace(R.id.fragment_main, MainFragment.newInstance(simple, compound)) commit() } } }
Although the highlighted line in the newInstance()
method of MainFragment
shows that we’re adding a Parcelable
to the Bundle
, it only gets flattened if the Android framework needs to flatten it across a process boundary. In this context, where it’s being passed within the same Activity
there is no reason the flatten it, so the same instance of the CompoundDataClass
is retrieved in the highlighted line in the onCreate()
method.
We can simulate flattening of the Parcel
by adding a couple of extension functions which mimic how the Android framework will flatten and expand Parcels
:
inline funT.collapse(): ByteArray { val parcel = Parcel.obtain() parcel.writeParcelable(this, 0) val byteArray = parcel.marshall() parcel.recycle() return byteArray } inline fun Class .expand(byteArray: ByteArray): T { val parcel = Parcel.obtain() parcel.apply { unmarshall(byteArray, 0, byteArray.size) setDataPosition(0) } val parcelable = parcel.readParcelable ([email protected]) ?: throw InstantiationException("Unable to expand $name") parcel.recycle() return parcelable }
We can then invoke these to forcefully flatten the data class, and expand it again (this really isn’t something that anyone would need to do in real world scenarios, so please heed the comments on the code):
@JvmStatic fun newInstance(simpleDataClass: SimpleDataClass, compound: CompoundDataClass) = MainFragment().apply { arguments = Bundle().apply { putParcelable(ARG_SIMPLE, simpleDataClass) putParcelable( ARG_COMPOUND, /* * Don't do this. * * I've done this here in some sample code to demonstrate * that things don't always get serialised. There is no * reason than you'd actually want to do this in this context. * * So...really...don't do this. * * Look, I'm not joking, you really shouldn't do this. * * Even if you're being attacked by a pack of wild dogs and * think that collapsing then immediately re-expanding a * Parcelable will save your life, then I'm sorry, but * it won't. Rest In Peace. * * Perhaps I forgot to mention: you really shouldn't do this. */ CompoundDataClass::class.java.expand(compound.collapse()) ) } }
Even if we do that, we still get the value of “Transient” in the ARG_COMPOUND
value that we get from the Fragment
arguments in the onCreate()
method of MainFragment
. As I mentioned previously this is because the bytecode generated by @Parcelize
will persist all fields including those marked as @Transient
this is slightly inconsistent with how transient fields are usually treated during persistence, but we can prove this by looking at a decompilation of the Kotlin bytecode:
@Parcelize public final class CompoundDataClass implements Parcelable { @NotNull private final String name; @NotNull private final SimpleDataClass simpleDataClass; @NotNull private final transient String transientString; public static final android.os.Parcelable.Creator CREATOR = new CompoundDataClass.Creator(); . . . public void writeToParcel(@NotNull Parcel parcel, int flags) { Intrinsics.checkParameterIsNotNull(parcel, "parcel"); parcel.writeString(this.name); this.simpleDataClass.writeToParcel(parcel, 0); parcel.writeString(this.transientString); } @Metadata( mv = {1, 1, 15}, bv = {1, 0, 3}, k = 3 ) public static class Creator implements android.os.Parcelable.Creator { @NotNull public final Object[] newArray(int size) { return new CompoundDataClass[size]; } @NotNull public final Object createFromParcel(@NotNull Parcel in) { Intrinsics.checkParameterIsNotNull(in, "in"); return new CompoundDataClass(in.readString(), (SimpleDataClass)SimpleDataClass.CREATOR.createFromParcel(in), in.readString()); } } }
The field named transientString
gets serialised to and from the Parcel
despite being declared using the Java transient
keyword. If we want to have transience for this field we have to do things manually. It is actually worth covering this because we may also need to do this if we have fields which are third-party objects which are not Parcelable
and we need to implement a custom mechanism for storing sufficient data to the Parcel
to allow us to re-instantiate a specific object when the Parcel
is expanded back to object instances:
@Parcelize data class CompoundDataClass @JvmOverloads constructor( val name: String, val simpleDataClass: SimpleDataClass, @Transient val transientString: String = "" ) : Parcelable { companion object : Parceler{ override fun create(parcel: Parcel): CompoundDataClass { val name: String = parcel.readString()!! val simple: SimpleDataClass = parcel.readParcelable(SimpleDataClass::class.java.classLoader)!! return CompoundDataClass(name, simple) } override fun CompoundDataClass.write(parcel: Parcel, flags: Int) { parcel.writeString(name) parcel.writeParcelable(simpleDataClass, flags) } } }
Having a Parceler
companion object which implements the create()
and CompoundDataClass.write()
methods enables us to customise what is persisted to the Parcel
and, in this case, allows us to omit the transientString
field. Although the @Transient
annotation achieves no direct purpose here, I think it is good to leave it here because it make it much clearer to anyone reading the code that the field will not be persisted. Our code is easier to maintain and understand as a result.
While it might be argued that having to implement these methods to perform the parcelization is back to where we were at the beginning (i.e. having to do things manually). However, if you compare this to the very first code snippet, still much of the boilerplate code is still generated for us, and there is still much less cruft as a result of using Parcelize
.
The source code for this article is available here.
© 2019, Mark Allison. All rights reserved.
Copyright © 2019 Styling Android. All Rights Reserved.
Information about how to reuse or republish this work may be available at http://blog.stylingandroid.com/license-information.