Kotlin / Parcelize

Parcelize

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 fun  T.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.

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.