In Jetpack Compose, composable functions often hold state using the remember
function. Values that are remembered can be reused across recompositions, as
explained in State and Jetpack Compose.
While remember serves as a tool to persist values across recompositions, state
often needs to live beyond the lifetime of a composition. This page explains the
difference between the remember, retain, rememberSaveable,
and rememberSerializable APIs, when to choose which API, and what the
best practices are for managing remembered and retained values in Compose.
Choose the correct lifespan
In Compose, there are several functions you can use to persist state across
compositions and beyond: remember, retain, rememberSaveable, and
rememberSerializable. These functions differ in their lifespan and semantics,
and are each suited for storing specific kinds of state. The differences are
outlined in the following table:
|
|
|
|
|---|---|---|---|
Values survive recompositions? |
✅ |
✅ |
✅ |
Values survive activity recreations? |
❌ |
✅ The same ( |
✅ An equivalent ( |
Values survive process death? |
❌ |
❌ |
✅ |
Supported data types |
All |
Must not reference any objects that would be leaked if the activity is destroyed |
Must be serializable |
Use cases |
|
|
|
remember
remember is the most common way to store state in Compose. When remember is
called for the first time, the given calculation is executed and is
remembered, meaning that it is stored by Compose for future reuse by the
composable. When a composable recomposes, it executes its code again, but any
calls to remember return their values from the previous composition instead of
executing the calculation again.
Each instance of a composable function has its own set of remembered values, referred to as positional memoization. When remembered values are memoized for use across recompositions, they are tied to their position in the composition hierarchy. If a composable is used in different locations, each instance in the composition hierarchy has its own set of remembered values.
When a remembered value is no longer used, it is forgotten and its record is
discarded. Remembered values are forgotten when they are removed from the
composition hierarchy (Including when a value is removed and re-added to move to
a different location without the use of the key composable or
MovableContent), or called with different key parameters.
Of the choices available, remember has the shortest lifespan and forgets
values the earliest of the four memoization functions described in this page.
This makes it best-suited for:
- Creating internal state objects, such as scroll position or animation state
- Avoiding expensive object recreation on each recomposition
However, you should avoid:
- Storing any user input with
remember, because remembered objects are forgotten across Activity configuration changes and system-initiated process death.
rememberSaveable and rememberSerializable
rememberSaveable and rememberSerializable build on top of remember. They
have the longest lifespan of the memoization functions discussed in this guide.
In addition to positionally memoizing objects across recompositions, it can also
save values so that they can be restored across activity recreations,
including from configuration changes and process death (when the system kills
your app's process while it is in the background, usually either to free memory
for foreground apps or if the user revokes permissions from your app while it is
running).
rememberSerializable works in the same way as rememberSaveable, but
automatically supports persisting complex types that are serializable with the
kotlinx.serialization library. Choose rememberSerializable if your type is
(or can be) marked with @Serializable, and rememberSaveable in all other
cases.
This makes both rememberSaveable and rememberSerializable perfect candidates
for storing state associated with user input, including text field entry, scroll
position, toggle states, etc. You should save this state to ensure that the user
never loses their place. In general, you should use rememberSaveable or
rememberSerializable to memoize any state that your app isn't able to retrieve
from another persistent data source, such as a database.
Note that rememberSaveable and rememberSerializable save their memoized
values by serializing them into a Bundle. This has two consequences:
- Values you memoize must be representable by one or more of the following data
types: Primitives (including
Int,Long,Float,Double),String, or arrays of any of these types. - When a saved value is restored, it will be a new instance that is equal to
(
==), but not the same reference (===) that the composition used before.
To store more complicated data types without using kotlinx.serialization, you
can implement a custom Saver to serialize and deserialize your object into
supported data types. Note that Compose understands common data types like
State, List, Map, Set, etc. out-of-the-box, and automatically converts
these into supported types on your behalf. The following is an example of a
Saver for a Size class. It's implemented by packing all of Size's
properties into a list using listSaver.
data class Size(val x: Int, val y: Int) { object Saver : androidx.compose.runtime.saveable.Saver<Size, Any> by listSaver( save = { listOf(it.x, it.y) }, restore = { Size(it[0], it[1]) } ) } @Composable fun rememberSize(x: Int, y: Int) { rememberSaveable(x, y, saver = Size.Saver) { Size(x, y) } }
retain
The retain API exists between remember and
rememberSaveable/rememberSerializable in terms of how long it memoizes its
values. It is named differently because retained values also experience a
different lifecycle than their remembered counterparts.
When a value is retained, it is both positionally memoized and saved in a
secondary data structure that has a separate lifespan that is tied to the app's
lifespan. A retained value is able to survive configuration changes without
being serialized, but cannot survive process death. If a value is not used after
the composition hierarchy is recreated, the retained value is retired (which
is retain's equivalent of being forgotten).
In exchange for this shorter-than-rememberSaveable lifecycle, retain is able
to persist values that can't be serialized, like lambda expressions, flows, and
large objects like bitmaps. For example, you can use retain to manage a media
player (such as ExoPlayer) to prevent interruptions to media playback during
a configuration change.
@Composable fun MediaPlayer() { // Use the application context to avoid a memory leak val applicationContext = LocalContext.current.applicationContext val exoPlayer = retain { ExoPlayer.Builder(applicationContext).apply { /* ... */ }.build() } // ... }
retain versus ViewModel
At their cores, both retain and ViewModel offer similar functionality in
their most commonly used ability to persist object instances across
configuration changes. The choice to reach for retain or ViewModel lies in
the type of value you're persisting, how it should be scoped, and whether you
need additional functionality.
ViewModels are objects that typically encapsulate the communication between
your app's UI and data layers. They allow you to move logic out of your
composable functions, which improves testability. ViewModels are managed as
singletons within a ViewModelStore and have a different lifespan from retained
values. While a ViewModel will remain active until its ViewModelStore is
destroyed, retained values are retired when the content is permanently removed
from the composition (for a configuration change, as an example, this means that
a retained value is retired if the UI hierarchy is recreated and the retained
value wasn't consumed after the composition is recreated).
ViewModel also includes out-of-the-box integrations for dependency injection
with Dagger and Hilt, integration with SavedState, and built-in coroutine
support for launching background tasks. This makes ViewModel an ideal place to
launch background tasks and network requests, interact with other data sources
in your project, and optionally capture and persist mission-critical UI state
that should be both retained across configuration changes in the ViewModel and
survive process death.
retain is best suited for objects that are scoped to specific composable
instances and don't require reuse or sharing between sibling composables. Where
ViewModel acts as a good place to store UI state and perform background tasks,
retain is a good candidate for storing objects for UI plumbing like caches,
impression tracking and analytics, dependencies on AndroidViews, and other
objects that interact with the Android OS or manage third party libraries like
payment processors or advertising.
For advanced users designing custom app architecture patterns outside of the
Modern Android app architecture recommendations: retain can also be used to
build an in-house "ViewModel-like" API. Although support for coroutines and
saved-state is not offered out-of-the-box, retain can serve as the building
block for the lifecycle of such ViewModel-look-alikes with these features
built on top. The specifics of how to design such a component are outside the
scope of this guide.
|
|
|
|---|---|---|
Scoping |
No shared values; each value is retained at and associated with a specific point in the composition hierarchy. Retaining the same type in a different location always acts on a new instance. |
|
Destruction |
When permanently leaving the composition hierarchy |
When the |
Additional functionality |
Can receive callbacks when the object is in the composition hierarchy or not |
Built-in |
Owned by |
|
|
Use cases |
|
|
Combine retain and rememberSaveable or rememberSerializable
Sometimes, an object needs to have a hybrid lifespan of both retained and
rememberSaveable or rememberSerializable. This may be an indicator that your
object should be a ViewModel, which can support saved state as described in
the Saved State module for ViewModel guide.
it is possible to use retain and rememberSaveable or rememberSerializable
simultaneously. Correctly combining both lifecycles adds significant complexity.
We recommend employing this pattern as part of more advanced and custom
architecture patterns, and only when all of the following are true:
- You are defining an object comprised of a mix of values that must be retained or saved (e.g. an object that tracks a user input and an in-memory cache that can't be written to disk)
- Your state is scoped to a composable and isn't suitable for the singleton
scoping or lifespan of
ViewModel
When all of these are the case, we recommend splitting your class into three parts: The saved data, the retained data, and a "mediator" object that has no state of its own and delegates onto the retained and saved objects to update state accordingly. This pattern takes the following shape:
@Composable fun rememberAndRetain(): CombinedRememberRetained { val saveData = rememberSerializable(serializer = serializer<ExtractedSaveData>()) { ExtractedSaveData() } val retainData = retain { ExtractedRetainData() } return remember(saveData, retainData) { CombinedRememberRetained(saveData, retainData) } } @Serializable data class ExtractedSaveData( // All values that should persist process death should be managed by this class. var savedData: AnotherSerializableType = defaultValue() ) class ExtractedRetainData { // All values that should be retained should appear in this class. // It's possible to manage a CoroutineScope using RetainObserver. // See the full sample for details. var retainedData = Any() } class CombinedRememberRetained( private val saveData: ExtractedSaveData, private val retainData: ExtractedRetainData, ) { fun doAction() { // Manipulate the retained and saved state as needed. } }
By separating the state by lifespan, the separation of responsibilities and
storage becomes very explicit. It is intentional that save data cannot be
manipulated by retain data, as this prevents a scenario where a save data update
is attempted when the savedInstanceState bundle has already been captured and
cannot be updated. It also allows for testing recreation scenarios by testing
your constructors without calling into Compose or simulating an Activity
recreation.
See the full sample (RetainAndSaveSample.kt) for a complete example of
how this pattern may be implemented.
Positional memoization and adaptive layouts
Android applications can support many form factors including phones, foldables, tablets, and desktops. Applications frequently need to transition between these form factors by using adaptive layouts. For example, an app running on a tablet may be able to show a two-column list-detail view, but may navigate between a list and detail page when presented on a smaller phone screen.
Because remembered and retained values are memoized positionally, they are only reused if they appear in the same point in the composition hierarchy. As your layouts adapt to different form factors, they may alter the structure of your composition hierarchy and lead to forgotten values.
For out-of-the-box components like ListDetailPaneScaffold and NavDisplay
(from Jetpack Navigation 3), this is not an issue and your state will persist
throughout layout changes. For custom components that adapt to form factors,
ensure that state is unaffected by layout changes by doing one of the following:
- Ensure that stateful composables are always called in the same place in the composition hierarchy. Implement adaptive layouts by altering the layout logic instead of relocating objects in the composition hierarchy.
- Use
MovableContentto relocate stateful composables gracefully. Instances ofMovableContentare able to move remembered and retained values from their old to new locations.
Remember factory functions
Although Compose UIs are made up of composable functions, many objects go into
the creation and organization of a composition. The most common example of this
is complex composable objects that define their own state, like LazyList,
which accepts a LazyListState.
When defining Compose-focused objects, we recommend creating a remember
function to define the intended remembering behavior, including both lifespan
and key inputs. This allows consumers of your state to confidently create
instances in the composition hierarchy that will survive and be invalidated as
expected. When defining a composable factory function, follow these guidelines:
- Prefix the function name with
remember. Optionally, if the function implementation depends on the object beingretainedand the API will never evolve to rely on a different variation ofremember, use theretainprefix instead. - Use
rememberSaveableorrememberSerializableif state persistence is chosen and it's possible to write a correctSaverimplementation. - Avoid side effects or initializing values based on
CompositionLocals that might not be relevant to the usage. Remember, the place your state is created might not be where it is consumed.
@Composable fun rememberImageState( imageUri: String, initialZoom: Float = 1f, initialPanX: Int = 0, initialPanY: Int = 0 ): ImageState { return rememberSaveable(imageUri, saver = ImageState.Saver) { ImageState( imageUri, initialZoom, initialPanX, initialPanY ) } } data class ImageState( val imageUri: String, val zoom: Float, val panX: Int, val panY: Int ) { object Saver : androidx.compose.runtime.saveable.Saver<ImageState, Any> by listSaver( save = { listOf(it.imageUri, it.zoom, it.panX, it.panY) }, restore = { ImageState(it[0] as String, it[1] as Float, it[2] as Int, it[3] as Int) } ) }