How to Correctly Save the State of a Custom View in Android

Some time ago I came across one problem related to the correct recreation of the state in the view. I tested various solutions on a separate project to learn about possible solutions. However, before I describe the exact problem that I came across, I will start with the complete basics. At the very beginning, let's try to answer the question - why should we actually save the state of the views? Imagine a situation in which you fill a large questionnaire in a certain application on your smartphone . At some point, you accidentally rotate the screen or you want to check something in another application and, after returning to the questionnaire, it turns out that all the fields are empty. Such a situation effectively discourage users from using the application. For the purposes of this example, let's create a view:

Instead of rotating the screen every time to check the effect, we can enable the appropriate option in the developer settings of the phone (Settings -> Developer options -> Don’t keep activities -> turn it on).

As you can see - the state has not been saved. Let’s recall the hierarchy that is called by Android to save and read the state:

It works! Great, let’s add two more our custom views. In the end, we created this whole thing so as not to have to duplicate the code and check if everything is still working.

Wait… what just happened? Well, looking at the implementation of saveHierarchyState and dispatchSaveInstanceState, we can observe that every state is stored in one container and is shared for the entire view hierarchy. Let's draw up the current hierarchy:

As you can see, the tags @+id/customViewSwitch and @+id/customViewEditText and are repeated, so the generated sparse array saves the children of @+id/switch1, then @+id/switch2, and finally @+id/switch3.

SparseArray maps integers to Objects and, unlike a normal array of Objects, its indices can contain gaps. SparseArray is intended to be more memory-efficient than a HashMap , because it avoids auto-boxing keys and its data structure doesn’t rely on an extra entry object for each mapping.

Let's check how the container behaves when we pass the same key again:

In the examples above, I write the super state returned by super.onSaveInstanceState() to a parcelable with the key SUPER_STATE_KEY and then create a custom sparse array the with state of every child in the view using the saveChildViewStates() extension function and saving it with a SPARSE_STATE_KEY key:

In the example above, I get the previously saved super state and pass it to the super.onRestoreInstanceState(). After getting the created sparse array using SPARSE_STATE_KEY, I use another extension function restoreChildViewStates:

In this example, I call the restoreHierarchyState function for each child to which I pass the previously saved SparseArray. Let’s check how the hierarchy looks now:

And check if everything is working:

Everything works well! This is one of the ways to save the state of your own view, but there is another interesting solution using the BaseSavedState class.

At the beginning, we should override the functions dispatchSaveInstanceState and dispatchRestoreInstanceState in the same way as we did before and then create an internal class that extends BaseSavedState:

Then create an internal variable childrenStates with the children’s states, handle saving the state inside writeToParcel, and write an inside constructor. It’s crucial to create a CREATOR object and override createFromParcel to provide the SavedState constructor and a newArray to provide an empty array of nulls.

The last thing is to override and handle onSaveInstanceState and onRestoreInstanceState:

Inside onRestoreInstanceState, I check if the provided parcelable is SavedState and then call super.onRestoreInstanceState providing superState, retrieve childrenStates, and pass it to the restoreChildViewStates extension function. Let's check if everything is working properly:

The advantages of using BaseSavedState are, among others, automatic superState handling and more intelligent memory management. Let’s compare the operation of both methods in the case of screen rotation and real loss of state (from developer settings). In each of the two methods, I put the logs at the time of calling onSaveInstanceState , onRestoreInstanceState and the places where the children's status is written to and read from the sparse array. I tagged the first method with the ByHand log tag, and the second with the SavedState tag:

Logs from rotating the screen:

I/ByHand: onSaveInstanceState
I/ByHand: Writing children state to sparse array
I/ByHand: onSaveInstanceState
I/ByHand: Writing children state to sparse array
I/ByHand: onSaveInstanceState
I/ByHand: Writing children state to sparse array
I/SavedState: onSaveInstanceState
I/SavedState: onSaveInstanceState
I/SavedState: onSaveInstanceState
I/ByHand: onRestoreInstanceState
I/ByHand: Reading children children state from sparse array
I/ByHand: onRestoreInstanceState
I/ByHand: Reading children children state from sparse array
I/ByHand: onRestoreInstanceState
I/ByHand: Reading children children state from sparse array
I/SavedState: onRestoreInstanceState
I/SavedState: onRestoreInstanceState
I/SavedState: onRestoreInstanceState

And from losing state using developer settings:

I/ByHand: onSaveInstanceState
I/ByHand: Writing children state to sparse array
I/ByHand: onSaveInstanceState
I/ByHand: Writing children state to sparse array
I/ByHand: onSaveInstanceState
I/ByHand: Writing children state to sparse array
I/SavedState: onSaveInstanceState
I/SavedState: onSaveInstanceState
I/SavedState: onSaveInstanceState
I/SavedState: Writing children state to sparse array
I/SavedState: Writing children state to sparse array
I/SavedState: Writing children state to sparse array
I/SavedState: Reading children children state from sparse array
I/SavedState: Reading children children state from sparse array
I/SavedState: Reading children children state from sparse array
I/ByHand: onRestoreInstanceState
I/ByHand: Reading children children state from sparse array
I/ByHand: onRestoreInstanceState
I/ByHand: Reading children children state from sparse array
I/ByHand: onRestoreInstanceState
I/ByHand: Reading children children state from sparse array
I/SavedState: onRestoreInstanceState
I/SavedState: onRestoreInstanceState
I/SavedState: onRestoreInstanceState

It’s worth noting that when the screen is rotated using BaseSavedState, the state of children not saved to the sparse array, but kept in memory, which I think is a good solution taking into consideration the fact that during screen rotation the system doesn’t really need to release all the memory which does not cause unnecessary overhead.

As we can see now, saving the state of your own view is not a particularly complicated process, and it significantly improves the user's experience.