Jetpack Compose rememberSaveable: Less Boilerplate, But Know the Limits
rememberSaveable removes onSaveInstanceState boilerplate in Jetpack Compose, but it serializes into a Bundle with strict size limits and no guarantee across process death. Learn when to use it, how to write custom Savers, and how to verify state actually survives rotation.
16 Sept 2026, 02:58 UTC

The Problem: Configuration Changes Still Eat Your State
Rotate a phone, split-screen an app, or switch languages — Android destroys and recreates your Activity. In the old View system you overrode onSaveInstanceState, stuffed a Bundle, and pulled it back in onCreate. Jetpack Compose promised to make this disappear. rememberSaveable does remove the ceremony, but it trades one set of constraints for another. If you treat it as magic, you will ship bugs that only appear when a user rotates their device while on a flaky train connection.
How It Works Under the Hood
rememberSaveable is a thin wrapper around remember that registers a SavedStateRegistry consumer. When the system asks your composable to save state, the registry serializes every registered value into a Bundle using the same machinery that backs ViewModel saved state. On recreation the same keys are read back and the remembered slot is re-initialized. The API surface is tiny:
@Composable
inline fun rememberSaveable(
keys: List = emptyList(),
stateSaver: Saver = autoSaver(),
init: () -> T
): T
The stateSaver parameter is where the contract lives. Compose ships savers for primitives, Parcelable, Serializable, ArrayList, SparseArray, and a handful of framework types. Anything else requires you to supply a Saver — two lambdas that convert your object to and from something the Bundle understands.
Built-In Types vs. Custom Serializers
A data class User(val name: String, val id: Long) implements Serializable automatically, so rememberSaveable { User("", 0L) } works out of the box. But the moment you add a non-serializable field — a Bitmap, a Room entity with a @Relation, or a sealed interface — the compiler will not warn you. The crash arrives at runtime during onSaveInstanceState with a ParcelableException.
Fix it by writing an explicit Saver:
val userSaver = Saver>(
save = { mapOf("name" to it.name, "id" to it.id) },
restore = { User(it["name"] as String, it["id"] as Long) }
)
var user by rememberSaveable(stateSaver = userSaver) { User("", 0L) }
Keep the saved representation flat and primitive. Nested maps work, but each level adds reflection overhead during the critical path of activity recreation.
Worked Example: TextField That Survives Rotation
The canonical use case is a search screen where the user has typed a query, rotates to landscape, and expects the text to still be there.
@Composable
fun SearchScreen(onSearch: (String) -> Unit) {
var query by rememberSaveable { mutableStateOf("") }
Column(modifier = Modifier.padding(16.dp)) {
TextField(
value = query,
onValueChange = { query = it },
label = { Text("Search") },
modifier = Modifier.fillMaxWidth()
)
Button(onClick = { onSearch(query) }, modifier = Modifier
.fillMaxWidth()
.padding(top = 16.dp)) {
Text("Go")
}
}
}
No onSaveInstanceState, no ViewModel for this trivial state. Rotate the device (or use adb shell am start -n ... --activity-clear-task to simulate process death) and the query remains. The saved key in the Bundle will be something like androidx.compose.ui.savedstate.Saver_0 — opaque but inspectable.
Trade-Offs You Will Hit in Production
- Bundle size limit: The transaction buffer for
onSaveInstanceStateis roughly 50 KB on most API levels. A list of 200Userobjects serialized as maps will exceed it, causingTransactionTooLargeExceptionand silent state loss. If your UI state grows, move it to aViewModelbacked bySavedStateHandleor persist to disk. - Process death ≠ configuration change:
rememberSaveableonly guarantees survival across configuration changes. If the OS kills your process for memory, theBundleis still delivered — but only if it was small enough to be written before the kill. Large bundles are dropped silently. - Recomposition timing: The restored value is available during the first composition after recreation. Reading it in a
LaunchedEffect(Unit)that runs before the restore completes will give you the initial value, not the saved one. UseLaunchedEffect(key1 = restoredValue)or observe the state directly in composables.
Verify It Before You Ship
- Rotate the device (or toggle "Don't keep activities" in Developer Options). The
TextFieldcontent must not flicker or reset. - Open Android Studio's Memory Profiler, trigger a dump, and search the
Bundlefor your custom keys. Confirm the serialized form matches yourSaveroutput. - Run
adb shell dumpsys activity activities | grep -A 20 'Saved State'to see the raw bundle keys and sizes on a physical device.
If the bundle exceeds ~40 KB, refactor. rememberSaveable is a convenience for ephemeral UI state — scroll position, text input, toggle selections — not a replacement for architectural state management.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.