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