Persist UI State Across Configuration Changes with rememberSaveable in Jetpack Compose
Learn how to use Jetpack Compose's rememberSaveable to keep UI state like text field content or custom data objects alive across screen rotations and process death, with a worked example and verification steps.
01 Jul 2025, 03:57 UTC

Why rememberSaveable matters
When a Compose UI is hosted in an Activity or Fragment, configuration changes such as screen rotation or language switches cause the composable tree to be recreated. If you rely only on local variables, any UI state (e.g., the text a user typed) is lost. rememberSaveable solves this by automatically saving and restoring the value you give it, using Android’s SavedStateHandle mechanism, which survives both recompositions and process death.
Basic usage example
The simplest case works for primitive types and Parcelable objects. The following composable keeps the content of a TextField after a rotation:
@Composable
fun NoteEditor() {
// rememberSaveable saves the String across configuration changes
var text by rememberSaveable { mutableStateOf("") }
Column(modifier = Modifier.padding(16.dp)) {
TextField(
value = text,
onValueChange = { text = it },
label = { Text("Note") }
)
Text("Current length: ${text.length}", modifier = Modifier.topMargin(8.dp))
}
}
No extra code is required because String is a built‑in type that Compose knows how to store in a Bundle.
Persisting custom objects with a Saver
For data classes that are not Parcelable, you must supply a Saver that tells Compose how to turn the object into a Bundle-compatible representation and back. The example below persists a simple Task data class:
data class Task(val id: Long, val title: String, val isDone: Boolean)
// Define how to convert Task to/from a Bundle‑friendly map
private val TaskSaver = object : Saver> {
override fun save(value: Task): Map = mapOf(
"id" to value.id,
"title" to value.title,
"isDone" to value.isDone
)
override fun restore(value: Map): Task = Task(
value["id"] as Long,
value["title"] as String,
value["isDone"] as Boolean
)
}
@Composable
fun TaskEditor(taskId: Long) {
var task by rememberSaveable(saver = TaskSaver) {
// In a real app you would load the task from a repository here
mutableStateOf(Task(taskId, "", false))
}
Column(modifier = Modifier.padding(16.dp)) {
TextField(
value = task.title,
onValueChange = { task = task.copy(title = it) },
label = { Text("Title") }
)
Checkbox(
checked = task.isDone,
onCheckedChange = { task = task.copy(isDone = it) }
)
Text("Task ID: ${task.id}", modifier = Modifier.topMargin(8.dp))
}
}
The Saver is passed as the saver argument to rememberSaveable. Without it, Compose would throw an IllegalArgumentException at runtime because it cannot serialize the Task object.
Key parameter and scoping
If you place multiple instances of the same composable in the UI tree (e.g., a list of editors), you can give each a stable key so that their saved states do not clash:
@Composable
fun TaskList() {
val tasks = listOf(1L, 2L, 3L) // example IDs
tasks.forEach { id ->
TaskEditor(taskId = id, key = "task_$id") // key scopes the saved state
}
}
@Composable
fun TaskEditor(taskId: Long, key: String) {
var task by rememberSaveable(
saver = TaskSaver,
key = key
) { mutableStateOf(Task(taskId, "", false)) }
// … UI as before …
}
The key must be constant across recompositions; using a changing value (like System.currentTimeMillis()) would prevent restoration.
Limitations and common pitfalls
- Host requirement:
rememberSaveableonly works when the composable tree is attached to aComponentActivityor aFragmentthat usesComposeView. In a plainViewhierarchy without such a host, the saved state is discarded. - Bundle size: The underlying
Bundlehas a practical limit of about 1 MB. Storing large bitmaps, long lists, or complex objects directly can cause aTransactionTooLargeException. Keep saved state small; defer heavy data to a ViewModel or repository. - Missing Saver: Passing a non‑
Parcelablecustom type without aSaver results in anIllegalArgumentExceptionat runtime. The stack trace will mention "cannot save" the class name. Fix by providing aSaveror making the typeParcelable. - Key collisions: If you reuse the same key for two different composables that should have independent state, the later one will overwrite the former’s saved value. Ensure keys are unique within the same scope.
How to verify the behavior
- Rotation test: Run the app on an emulator or physical device, edit the
TextField(or modify theTaskfields), then rotate the screen. The edited value should remain unchanged after the rotation. - Process‑kill test: Enable Developer options → Don’t keep activities. Navigate away from the screen and then return. If a proper
Saverwas supplied, the state is restored; otherwise it resets to the initial value. - Logcat check: While testing a custom type without a
Saver, look for lines likejava.lang.IllegalArgumentException: Class ... is not supported by rememberSaveable. After adding the correctSaver, the error should disappear.
These steps give you confidence that rememberSaveable is persisting UI state as expected across the most common configuration changes and even process death.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.