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.
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:
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:
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:
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
RetainedStateProviderleaves 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:
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.