Handle Android State Changes in Inherited Custom View
Works with Non-inherited and Inherited Custom View
In Android, there are some behaviors that we called “State Changes” which can occur from many conditions. Whether the platform might kill the application to reclaim the memory for the currently active app or Configuration Changes which recreated the Context in running Activity with the new configurations. To keep the data in your application from these changes, we have to handle the state in each Activity, Fragment and View.
Using existing Views from Android Frameworks or 3rd-party Libraries (which handle the state changes correctly), the state changes will be handled by itself. But when you create your own (called “Custom View”) you have to put the additional code for handling the state changes in your Custom View.
For example, my PostView
contains 3 states (title
, description
and dividerColorResId
) inside.
To support the state changes, I have to implement onSaveInstanceState
and onRestoreInstanceState
and handle all states in there.
As you see, handling state changes, you need SavedState
which extends from BaseSavedState
to transform the data to Parcelable when onSaveInstanceState
called, and onRestoreInstanceState
to restore the data back from Parcelable.
But the Android system does not hold the Parcelable while the state changes directly. Parcelable will be converted to Parcel and held in the system, and convert back to Parcelable when state restoring. That is why we have to declare the CREATOR
which create from Parcelable.Creator
to convert Parcel back to SavedState
(Parcelable) by createFromParcel
This code is a popular example of handling state changes in Custom View.
Unfortunately, it is not working with Inherited Custom View.
Inherited Custom View, How?
Instead of a single (non-inherited) Custom View, separated the code into a base class and derived class.
The code in BasePostView
will be
And RegularPostView
will be
These 2 classes handle state changes in the same way with a single Custom View. title
and description
handled by BasePostView
and dividerColorResId
handled by RegularPostView
.
And everything seems to work well until the app crashes while state changes by the application process.
What is going on?
Using BaseSavedState
for SavedState
and Parcelable.Creator
for CREATOR
in Inherited Custom View only works well when the state changes by configuration changes, not state changes by the application process.
In Android, when users put your app in the background for a long time, the Android system might reclaim the memory by killing your application’s process. But also be able to restore the data when the user opens your app again.
When restored from state changes by the application process, ClassLoader in CREATOR
is required for Parcel to Parcelable converting. But there is no ClassLoader in the above example. So RegularPostView
can not restore the state in BasePostView
(from super
).
Restoring from state changes by the application process, ClassLoader in CREATOR
is required for Parcel to Parcelable converting. But there is no ClassLoader in the above example. So RegularPostView
can not restore the state in BasePostView
(from super
).
To test your app in this behavior, just put your app into background, Click at Terminates selected Android Application
in Android Studio’s Logcat, then re-open your app again.
Before talking about how to solve this problem. Let’s talk about…
Why am I using the Inherited Custom View instead of a single Custom View?
From the previous example code, you might say “Just keep all code in one class because it’s Custom View, nothing more”.
My reason for using the Inherited Custom View is the RoundCornerProgressBar library for the customized progress bar.
In this library, I created 6 types of progress bars with some logic in common. So using Inherited Custom View is more suitable to maintain it in the future. (reusable + maintainable = 👍👍👍)
All classes also have their own states to handle. So BaseSavedState
and Parcelable.Creator
do not work for my library anymore.
Then, let’s get back to the previous code.
To solve this problem in Inherited Custom View
First, replace the BaseSavedState
with AbsSavedState
(which allows you to ensure the state of all classes along the hierarchy is saved) to all classes in Custom View’s hierarchy.
AbsSavedState
also has a constructor(source: Parcel, loader: ClassLoader?)
which supports ClassLoader. And must be AbsSavedState
from androidx.customview.view.AbsSavedState
for backward compatibility because the constructor of android.view.AbsSavedState
is available in API level 24 or higher.
androidx.customview.view.AbsSavedState
is in the CustomView library of Android Jetpack libraries and also included in the AppCompat library.
Second, replace the Parcelable.Creator
with Parcelable.ClassLoader
(which allows the Creator to receive the ClassLoader the object is being created in) to all classes in Custom View’s hierarchy.
Then, test the Custom View in every state changes behavior again. And it finally works! 🎉🎉🎉
Conclusion
To handle state changes in Inherited Custom View, Using AbsSavedState
and Parcelable.ClassLoaderCreator
which allows us to save/restore the state of all classes along the hierarchy.
Even the single Custom View, if your project contains both Custom Views, using AbsSavedState
and Parcelable.ClassLoaderCreator
in all of them is better than separating the code style between non-inherited and inherited.
No Inherited Custom View in your project? Don’t worry. BaseSavedState
and Parcelable.Creator
still work well for your Custom View, no need to replace them.
Finally, the example code of this article. See the link below