How to save Android View state in Kotlin

¯\_(ツ)_/¯

This article is inspired by the following great article that many Android devs have likely seen before:

Disclaimer: this is a real story happened to me but it might be different for other people.

Let’s say you follow that article step by step and create a view that has default(collapsed) and expanded states.

default(collapsed) state
expanded state

You likely have a field that holds a value to keep track of current state:

private boolean isExpanded;

And to be make view survive screen rotation you likely write something like this:

@Override
public Parcelable onSaveInstanceState() {
SavedState savedState = new SavedState(super.onSaveInstanceState());
savedState.isExpanded = isExpanded;
return savedState;
}

@Override
public void onRestoreInstanceState(Parcelable state) {
if (state instanceof SavedState) {
SavedState savedState = (SavedState) state;
super.onRestoreInstanceState(savedState.getSuperState());
isExpanded = savedState.isExpanded;
} else {
super.onRestoreInstanceState(state);
}
}

static class SavedState extends BaseSavedState {

boolean isExpanded;

SavedState(Parcel source) {
super(source);
isExpanded = source.readByte() != 0;
}

SavedState(Parcelable superState) {
super(superState);
}

@Override
public void writeToParcel(Parcel out, int flags) {
super.writeToParcel(out, flags);
out.writeByte((byte) (isExpanded ? 1 : 0));
}

public final static Creator<SavedState> CREATOR = new Creator<SavedState>() {

@Override
public SavedState createFromParcel(Parcel source) {
return new SavedState(source);
}

public SavedState[] newArray(int size) {
return new SavedState[size];
}
};
}

Note: if some of the code above is not clear to you, please refer to the article posted at the top

Everything works and Java but it’s time to move forward and write code in Kotlin. Let’s say you’ve decided to convert your view and used build-in tool.

yep, this one

You fix some red lines because converter isn’t perfect and now your field is all in Kotlin:

private var isExpanded = false

And code to save and restore states looks pretty Kotlin too:

public override fun onSaveInstanceState(): Parcelable? {
val savedState = SavedState(super.onSaveInstanceState())
savedState.isExpanded = isExpanded
return savedState
}

public override fun onRestoreInstanceState(state: Parcelable) {
if (state is SavedState) {
super.onRestoreInstanceState(state.superState)
isExpanded = state.isExpanded
} else {
super.onRestoreInstanceState(state)
}
}

internal class SavedState : View.BaseSavedState {

var isExpanded: Boolean = false

constructor(source: Parcel) : super(source) {
isExpanded = source.readByte().toInt() != 0
}

constructor(superState: Parcelable) : super(superState)

override fun writeToParcel(out: Parcel, flags: Int) {
super.writeToParcel(out, flags)
out.writeByte((if (isExpanded) 1 else 0).toByte())
}

val CREATOR: Parcelable.Creator<SavedState> = object : Parcelable.Creator<SavedState> {

override fun createFromParcel(source: Parcel): SavedState {
return SavedState(source)
}

override fun newArray(size: Int): Array<SavedState?> {
return arrayOfNulls(size)
}
}
}

You’ve run your code assuming nothing changed but then you get this nasty crash:

java.lang.RuntimeException: Parcel android.os.Parcel@<some id>: Unmarshalling unknown type code <some number> at offset <another number>

Then you go to google an try to find the reason but all stackoverflow answers tell that you write one type into parcel but trying to read a different type (example 1, example 2 and more) which is not your case.

The root of the problem here is Parcelable.Creator class. If you look at Parcelable.Creator docs you will the the following:

Interface that must be implemented and provided as a public CREATOR field that generates instances of your Parcelable class from a Parcel.

Then you look into the code and see

val CREATOR: Parcelable.Creator<SavedState> = ...

CREATOR is a public field in Kotlin… not in Java.
Android Studio has very useful tool to inspect how Kotlin bytecode is represented in Java. The tool is located in “Tools → Kotlin → Show Kotlin Bytecode”. This will generate bytecode that you can then decompile into Java. After you do that you should see:

public static final class SavedState extends BaseSavedState {
@NotNull
private final Creator CREATOR;
@NotNull
public final Creator getCREATOR() {
return this.CREATOR;
}
...
}

Public field in Kotlin is an equivalent of private field with a public getter in Java. This is basically why you get the crash in the first place. To fix that you need to use @JvmField annotation that according to docs:

Instructs the Kotlin compiler not to generate getters/setters for this property and expose it as a field.

Looking at decompiled Java code after adding @JvmField annotation you could see the following:

public static final class SavedState extends BaseSavedState {
@JvmField
@NotNull
public final Creator CREATOR;
...
}

It’s a public field now but there’s one more thing. If you look at Parcelable docs you will see:

Interface for classes whose instances can be written to and restored from a Parcel. Classes implementing the Parcelable interface must also have a non-null static field called CREATOR of a type that implements the Parcelable.Creator interface.

The field is not yet static so if run the code it’ll still crash with a following stacktrace:

android.os.BadParcelableException: Parcelable protocol requires the CREATOR object to be static on class

To achieve that you could use Kotlin’s companion object. Take a look at the docs and you’ll find that:

a companion object is initialized when the corresponding class is loaded (resolved), matching the semantics of a Java static initializer

Wrap it around CREATOR field and that’s it!

companion object {
@JvmField
val CREATOR = object : Parcelable.Creator<SavedState> {
override fun createFromParcel(source: Parcel): SavedState {
return SavedState(source)
}

override fun newArray(size: Int): Array<SavedState?> {
return arrayOfNulls(size)
}
}
}

You won’t get crashes in this place, view will save an restore state correctly. If you look at decompiled Java code you’ll see that the field is public and static which is exactly what Parcelable interface needs.

public static final class SavedState extends BaseSavedState {
@JvmField
@NotNull
public static final Creator CREATOR = ...
...
}

Big thanks to Abhi https://github.com/abhiin1947 for showing me @JvmField and Olivia https://github.com/oliviaperryman for digging deep into this problem 🙌🏻

Mobile developer, snowboarder, dog lover

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store