rememberSaveable and Custom Savers in Jetpack Compose

Quick answer: use
rememberSaveablefor small UI state a user expects to keep after activity recreation or system-initiated process death: typed input, selected tabs, expansion state, scroll position, and IDs. For a custom type, save only Bundle-compatible values withlistSaver,mapSaver, or aSaver. Do not save large object graphs or repository data in it.
remember survives recomposition but forgets values when an activity is recreated. rememberSaveable adds Android saved-instance-state restoration. It is a UI continuity tool, not a replacement for a ViewModel, database, or network cache.
The official state-saving guide recommends saving the minimum UI state needed to restore the user’s place, such as input, selected IDs, and scroll position.
Choose the right lifetime
| State | Prefer | Why |
|---|---|---|
| Temporary value during recomposition | remember | It belongs only while the composable remains in composition. |
| Small UI input or selection that should restore | rememberSaveable | It uses saved instance state across recreation. |
| Business logic and screen state owned by a ViewModel | SavedStateHandle for small restorable UI element state | The ViewModel remains the state owner. |
| Large data, user records, lists, or durable preferences | Repository / database / DataStore | Bundles are limited and should not carry full data models. |
rememberSaveable does not restore a value when the user explicitly dismisses the activity from Recents. That loss is normally reasonable for transient UI state.
Save primitives and Strings directly
Bundle-compatible values work without a custom saver:
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.material3.Checkbox
import androidx.compose.material3.Text
@Composable
fun NewsletterOption() {
var receiveUpdates by rememberSaveable { mutableStateOf(false) }
Checkbox(
checked = receiveUpdates,
onCheckedChange = { receiveUpdates = it },
)
Text("Receive product updates")
}The state survives rotation and a system recreation. The UI still needs one source of truth: if the checkbox represents a server-backed preference, load the actual preference from the state holder and save the confirmed value through the data layer.
Use inputs when the source changes
rememberSaveable accepts inputs. A changed input invalidates its saved value and re-runs the initializer. This prevents state from one item leaking into another item’s UI:
@Composable
fun SearchField(initialQuery: String) {
var query by rememberSaveable(initialQuery) {
mutableStateOf(initialQuery)
}
// Render a text field with query.
}The parameter is called inputs; the analogous remember parameter is named keys. Use stable values that genuinely define which state belongs to this UI.
Save a custom type with listSaver
Use a saver when the object itself cannot be put in a Bundle, but its essential fields can. Here a map viewport saves only its zoom and offset:
import androidx.compose.runtime.saveable.listSaver
import androidx.compose.runtime.saveable.rememberSaveable
data class MapViewport(
val zoom: Float,
val offsetX: Int,
val offsetY: Int,
) {
companion object {
val Saver = listSaver(
save = { listOf(it.zoom, it.offsetX, it.offsetY) },
restore = { values ->
MapViewport(
zoom = values[0] as Float,
offsetX = values[1] as Int,
offsetY = values[2] as Int,
)
},
)
}
}
@Composable
fun rememberMapViewport(): MapViewport {
return rememberSaveable(saver = MapViewport.Saver) {
MapViewport(zoom = 1f, offsetX = 0, offsetY = 0)
}
}listSaver is concise for a fixed, well-documented field order. mapSaver is clearer when names make restoration safer:
val CitySaver = mapSaver(
save = { city -> mapOf("name" to city.name, "country" to city.country) },
restore = { values -> City(values.getValue("name") as String, values.getValue("country") as String) },
)Use Parcelize when an existing Android model is naturally Parcelable, but still save only lightweight UI information. The Compose state documentation covers all three choices.
Keep the Bundle small
Saved instance state is shared with other Activity APIs. Large lists, bitmaps, full API responses, and full screen models can cause TransactionTooLarge failures. Save an ID, list position, filter, or draft input; reload detailed data from the data layer.
For example, save selectedArticleId, not an entire Article plus comments. Save a LazyListState through its provided saver, not every rendered item. Lazy list state is already designed for this purpose.
Test restoration deliberately
- Rotate the device while editing text, choosing a filter, and scrolling.
- Enable “Don’t keep activities” in developer options or use
StateRestorationTesterfor targeted Compose tests. - Test changed
inputsso state resets for the correct item. - Verify restored UI re-fetches data from the source of truth rather than rendering stale saved models.
A practical rule
Save only the smallest UI state needed to return a user to the same place. Use rememberSaveable for UI-owned values, a custom saver for a compact representation, and persistent data sources for everything large, durable, or business-critical. State Hoisting explains where that ownership belongs.