Depending on where your state is hoisted to and the logic that is required, you can use different APIs to store and restore your UI state. Every app uses a combination of APIs to best achieve this.
Any Android app could lose its UI state due to activity or process recreation. This loss of state can occur because of the following events:
- Configuration changes. The activity is destroyed and recreated unless the configuration change is handled manually.
- System-initiated process death. The app is in the background and the device frees up resources (like memory) to be used by other processes.
Preserving the state after these events is essential for a positive user experience. Selecting which state to persist depends on your app's unique user flows. As a best practice, you should at least preserve user input and navigation-related state. Examples of this include the scroll position of a list, the ID of the item the user wants more detail about, the in-progress selection of user preferences, or input in text fields.
This page summarizes the APIs available to store UI state depending on where your state is hoisted to and the logic that needs it.
UI logic
If your state is hoisted in the UI, either in composable functions or plain
state holder classes scoped to the Composition, you can use
rememberSaveable
to retain state across activity and process recreation.
In the following snippet, rememberSaveable
is used to store a single boolean
UI element state:
@Composable fun ChatBubble( message: Message ) { var showDetails by rememberSaveable { mutableStateOf(false) } ClickableText( text = AnnotatedString(message.content), onClick = { showDetails = !showDetails } ) if (showDetails) { Text(message.timestamp) } }
showDetails
is a boolean variable that stores if the chat bubble is collapsed
or expanded.
rememberSaveable
stores UI element state in a Bundle
through the
saved instance state mechanism.
It is able to store primitive types to the bundle automatically. If your state
is held in a type that is not primitive, like a data class, you can use
different storing mechanisms, such as using the Parcelize
annotation,
using Compose APIs like listSaver
and mapSaver
, or implementing a
custom saver class extending Compose runtime Saver
class. See the Ways
to store state documentation to learn more about these methods.
In the following snippet, the rememberLazyListState
Compose
API stores LazyListState
, which consists of the scroll state of a
LazyColumn
or LazyRow
, using rememberSaveable
. It uses a
LazyListState.Saver
, which is a custom saver that is able to
store and restore the scroll state. After an activity or process recreation (for
example, after a configuration change like changing device orientation), the
scroll state is preserved.
@Composable fun rememberLazyListState( initialFirstVisibleItemIndex: Int = 0, initialFirstVisibleItemScrollOffset: Int = 0 ): LazyListState { return rememberSaveable(saver = LazyListState.Saver) { LazyListState( initialFirstVisibleItemIndex, initialFirstVisibleItemScrollOffset ) } }
Best practice
rememberSaveable
uses a Bundle
to store UI state, which is shared by
other APIs that also write to it, like onSaveInstanceState()
calls in
your activity. However, the size of this Bundle
is limited, and storing large
objects could lead to TransactionTooLarge
exceptions in runtime. This
can be particularly problematic in single Activity
apps where the same
Bundle
is being used across the app.
To avoid this type of crash, you should not store large complex objects or lists of objects in the bundle.
Instead, store the minimum state required, like IDs or keys, and use these to delegate restoring more complex UI state to other mechanisms, like persistent storage.
These design choices depend on the specific use cases for your app and how your users expect it to behave.
Verify state restoration
You can verify that the state stored with rememberSaveable
in your
Compose elements is correctly restored when the activity or process is
recreated. There are specific APIs to achieve this, such as
StateRestorationTester
. Check out the Testing documentation to
learn more.
Business logic
If your UI element state is hoisted to the ViewModel
because it is
required by business logic, you can use ViewModel
's APIs.
One of the main benefits of using a ViewModel
in your Android application is
that it handles configuration changes for free. When there is a configuration
change, and the activity is destroyed and recreated, the UI state hoisted to the
ViewModel
is kept in memory. After the recreation, the old ViewModel
instance is attached to the new activity instance.
However, a ViewModel
instance does not survive system-initiated process death.
To have the UI state survive this, use the Saved State module for
ViewModel, which contains the SavedStateHandle
API.
Best practice
SavedStateHandle
also uses the Bundle
mechanism to store UI state, so
you should only use it to store simple UI element state.
Screen UI state, which is produced by applying business rules and accessing
layers of your application other than UI, should not be stored in
SavedStateHandle
due to its potential complexity and size. You can use
different mechanisms to store complex or large data, like local persistent
storage. After a process recreation, the screen is recreated with the
restored transient state that was stored in SavedStateHandle
(if any), and the
screen UI state is produced again from the data layer.
SavedStateHandle
APIs
SavedStateHandle
has different APIs to store UI element state, most
notably:
Compose State |
saveable() |
---|---|
StateFlow |
getStateFlow() |
Compose State
Use the saveable
API of SavedStateHandle
to read and write UI element
state as MutableState
, so it survives activity and process recreation with
minimal code setup.
The saveable
API supports primitive types out of the box and receives a
stateSaver
parameter to use custom savers, just like rememberSaveable()
.
In the following snippet, message
stores the user input types into a
TextField
:
class ConversationViewModel( savedStateHandle: SavedStateHandle ) : ViewModel() { var message by savedStateHandle.saveable(stateSaver = TextFieldValue.Saver) { mutableStateOf(TextFieldValue("")) } private set fun update(newMessage: TextFieldValue) { message = newMessage } /*...*/ } val viewModel = ConversationViewModel(SavedStateHandle()) @Composable fun UserInput(/*...*/) { TextField( value = viewModel.message, onValueChange = { viewModel.update(it) } ) }
See the SavedStateHandle
documentation for more information on
using the saveable
API.
StateFlow
Use getStateFlow()
to store UI element state and consume it as a flow
from the SavedStateHandle
. The StateFlow
is read
only, and the API requires you to specify a key so you can replace the flow to
emit a new value. With the key you configured, you can retrieve the StateFlow
and collect the latest value.
In the following snippet, savedFilterType
is a StateFlow
variable that
stores a filter type applied to a list of chat channels in a chat app:
private const val CHANNEL_FILTER_SAVED_STATE_KEY = "ChannelFilterKey" class ChannelViewModel( channelsRepository: ChannelsRepository, private val savedStateHandle: SavedStateHandle ) : ViewModel() { private val savedFilterType: StateFlow<ChannelsFilterType> = savedStateHandle.getStateFlow( key = CHANNEL_FILTER_SAVED_STATE_KEY, initialValue = ChannelsFilterType.ALL_CHANNELS ) private val filteredChannels: Flow<List<Channel>> = combine(channelsRepository.getAll(), savedFilterType) { channels, type -> filter(channels, type) }.onStart { emit(emptyList()) } fun setFiltering(requestType: ChannelsFilterType) { savedStateHandle[CHANNEL_FILTER_SAVED_STATE_KEY] = requestType } /*...*/ } enum class ChannelsFilterType { ALL_CHANNELS, RECENT_CHANNELS, ARCHIVED_CHANNELS }
Every time the user selects a new filter type, setFiltering
is called. This
saves a new value in SavedStateHandle
stored with the key
_CHANNEL_FILTER_SAVED_STATE_KEY_
. savedFilterType
is a flow emitting the
latest value stored to the key. filteredChannels
is subscribed to the flow to
perform the channel filtering.
See the SavedStateHandle
documentation for more information on the
getStateFlow()
API.
Summary
The following table summarizes the APIs covered in this section, and when to use each to save UI state:
Event | UI logic | Business logic in a ViewModel |
---|---|---|
Configuration changes | rememberSaveable |
Automatic |
System-initiated process death | rememberSaveable |
SavedStateHandle |
The API to use depends on where the state is held and the logic that it
requires. For state that is used in UI logic, use rememberSaveable
. For
state that is used in business logic, if you hold it in a ViewModel
,
save it using SavedStateHandle
.
You should use the bundle APIs (rememberSaveable
and SavedStateHandle
) to
store small amounts of UI state. This data is the minimum necessary to restore
the UI back to its previous state, together with other storing mechanisms. For
example, if you store the ID of a profile the user was looking at in the bundle,
you can fetch heavy data, like profile details, from the data layer.
For more information on the different ways of saving UI state, see the general Saving UI State documentation and the data layer page of the architecture guide.
Recommended for you
- Note: link text is displayed when JavaScript is off
- Where to hoist state
- State and Jetpack Compose
- Lists and grids