Android development
screen orientation
custom views
state management
mobile app development

How to prevent custom views from losing state across screen orientation changes

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

Introduction

On Android, an orientation change usually destroys and recreates the activity. If a custom view holds its own UI state, that state disappears unless the view participates in the normal save-and-restore mechanism. The correct fix is to save view-owned state in the view itself and keep screen-level data in a ViewModel or other higher-level state holder.

Use onSaveInstanceState and onRestoreInstanceState

A custom view should save its own transient UI state by overriding onSaveInstanceState() and onRestoreInstanceState().

kotlin
1import android.content.Context
2import android.os.Parcel
3import android.os.Parcelable
4import android.util.AttributeSet
5import android.view.View
6import androidx.customview.view.AbsSavedState
7
8class CounterView @JvmOverloads constructor(
9    context: Context,
10    attrs: AttributeSet? = null
11) : View(context, attrs) {
12
13    var count: Int = 0
14        set(value) {
15            field = value
16            invalidate()
17        }
18
19    override fun onSaveInstanceState(): Parcelable {
20        val superState = super.onSaveInstanceState()
21        return SavedState(superState).also { it.count = count }
22    }
23
24    override fun onRestoreInstanceState(state: Parcelable?) {
25        if (state !is SavedState) {
26            super.onRestoreInstanceState(state)
27            return
28        }
29        super.onRestoreInstanceState(state.superState)
30        count = state.count
31    }
32
33    internal class SavedState : AbsSavedState {
34        var count: Int = 0
35
36        constructor(superState: Parcelable?) : super(superState)
37
38        constructor(source: Parcel, loader: ClassLoader?) : super(source, loader) {
39            count = source.readInt()
40        }
41
42        override fun writeToParcel(out: Parcel, flags: Int) {
43            super.writeToParcel(out, flags)
44            out.writeInt(count)
45        }
46
47        companion object {
48            @JvmField
49            val CREATOR = object : ClassLoaderCreator<SavedState> {
50                override fun createFromParcel(source: Parcel, loader: ClassLoader?): SavedState {
51                    return SavedState(source, loader)
52                }
53
54                override fun createFromParcel(source: Parcel): SavedState {
55                    return SavedState(source, null)
56                }
57
58                override fun newArray(size: Int): Array<SavedState?> {
59                    return arrayOfNulls(size)
60                }
61            }
62        }
63    }
64}

This lets Android save and restore the custom view along with the rest of the view hierarchy.

Give the View a Stable ID

For view state restoration to work reliably, the custom view should have a stable ID in the layout.

xml
1<com.example.CounterView
2    android:id="@+id/counterView"
3    android:layout_width="120dp"
4    android:layout_height="120dp" />

Without an ID, the framework may not know how to match the restored state back to the recreated view instance.

Decide What State Belongs in the View

Not all state should live in a custom view. A good rule is:

  • view-owned UI state belongs in the view's saved state
  • screen or business state belongs in a ViewModel, repository, or other higher layer

For example, the current scroll offset of a custom timeline view belongs in the view. A list of domain objects loaded from the database does not.

If you store too much in the view's saved state, restoration becomes fragile and hard to maintain.

Use a ViewModel for Screen-Level Data

If the custom view renders data that should survive rotation independently of the view hierarchy, keep that data in a ViewModel and rebind it when the new activity instance is created.

kotlin
class CounterViewModel : ViewModel() {
    val count = MutableLiveData(0)
}

The custom view can still save lightweight presentation state such as selection or expansion position, while the ViewModel owns the underlying screen data.

Avoid configChanges as the Default Fix

Some developers try to stop recreation entirely by handling configuration changes manually in the manifest. That can be appropriate in special cases, but it is usually the wrong default for ordinary state loss in a custom view.

If the problem is that the view does not save its state correctly, fix the state-saving behavior. Do not bypass the lifecycle just to hide the bug.

Common Pitfalls

The most common mistake is storing custom view state in normal fields and expecting it to survive rotation automatically. It will not survive activity recreation unless you save and restore it.

Another issue is forgetting to call through to super.onSaveInstanceState() and super.onRestoreInstanceState(). That can break restoration higher up the view hierarchy.

Developers also often omit a stable view ID, which makes restoration unreliable.

Finally, avoid stuffing large domain objects into custom view state. Use a ViewModel for screen data and keep the view's saved state focused on UI behavior.

Summary

  • Save custom-view UI state with onSaveInstanceState() and onRestoreInstanceState().
  • Use a SavedState class derived from AbsSavedState.
  • Give the view a stable ID in the layout.
  • Keep view-owned UI state in the view and screen data in a ViewModel.
  • Fix state saving directly instead of using configChanges as a shortcut.

Course illustration
Course illustration

All Rights Reserved.