Skip to content

Retained state

rememberRetained is remember that survives configuration changes and navigating away and back. It is how a presenter keeps state without you writing a ViewModel.

var count by rememberRetained { mutableIntStateOf(0) }

Rotate the device and count is still there. Navigate to another screen and back and it is still there. Kill the process and it is gone — that is what saveable state is for.

Three kinds of state

API Survives recomposition Survives rotation Survives process death
remember yes no no
rememberRetained yes yes no
rememberSaveable yes yes yes

rememberRetained sits in the middle, and unlike rememberSaveable it holds arbitrary objects — no Saver, no serialization, no size limit.

val repository = rememberRetained { DetailRepository(screen.id) }
val scope = rememberRetained { CoroutineScope(SupervisorJob()) }

Inputs and keys

The signature mirrors rememberSaveable:

val detail = rememberRetained(screen.id) { DetailHolder(screen.id) }

Passing screen.id as an input discards the retained value when the id changes, exactly as remember(key) does.

The key parameter names the slot explicitly:

val left = rememberRetained(key = "left") { mutableIntStateOf(0) }
val right = rememberRetained(key = "right") { mutableIntStateOf(0) }

Without an explicit key, Orbit derives one from the composition key hash. That is fine in normal code. Give an explicit key when the same call site can appear more than once in a way Compose cannot distinguish, or when you want the slot to be stable no matter how you restructure the composition around it.

Scoping

Retention is hierarchical. ProvideOrbit installs a root registry backed by a ViewModel, which is what makes it outlive configuration changes:

ProvideOrbit(orbit) {
    ...
}

NavigableOrbitContent then gives each back stack record its own child registry keyed by the record's key. That is why two instances of the same screen keep independent state, and why a popped screen's state is discarded rather than reappearing on the next push.

You can create your own scope for a sub-tree:

RetainedStateProvider(key = "editor") {
    EditorContent()
}

Everything inside gets a nested registry that is dropped when the provider leaves for good.

Lifetime

A retained value is released when:

  • its back stack record is popped,
  • its RetainedStateProvider leaves the composition permanently,
  • the hosting ViewModel is cleared, meaning the activity finished for real.

It is not released on a configuration change, which is the entire point. If you hold something that needs closing, do it in an onCleared-style wrapper rather than assuming disposal:

val scope = rememberRetained { CoroutineScope(SupervisorJob()) }
DisposableEffect(Unit) { onDispose { } }

Do not retain a Context or a View

A retained value outlives the activity instance across a rotation. Holding an Activity, Context or View leaks it. Retain data and coroutine scopes, not framework objects.

Retained versus saveable

Use rememberSaveable when the value must come back after Android kills the process — a search query, a form draft, a scroll position. Use rememberRetained for everything expensive or non-serializable that only needs to outlive a rotation.

They compose well together:

var query by rememberSaveable { mutableStateOf("") }
val results = rememberRetained { ResultCache() }

The back stack itself is saveable, so after process death the stack rebuilds and each screen starts with fresh retained state — which is normally exactly what you want.

Direct registry access

The underlying registry is available if you need it:

val registry = LocalRetainedStateRegistry.current
registry.forgetValue("editor")

NoOpRetainedStateRegistry is the default when nothing has been provided; rememberRetained against it behaves like a plain remember, which keeps previews and tests working without setup.