根据您的状态被提升到的位置以及所需的逻辑,您可以使用不同的 API 来存储和恢复您的界面状态。每个应用都会组合使用不同的 API 来实现最佳效果。
任何 Android 应用都可能因 Activity 或进程重建而丢失其界面状态。状态丢失可能由以下事件引起:
在这些事件之后保留状态对于积极的用户体验至关重要。选择要持久化哪些状态取决于您应用的独特用户流程。作为最佳实践,您至少应保留用户输入和与导航相关的状态。这方面的示例包括列表的滚动位置、用户想要了解更多详细信息的项目 ID、用户偏好设置的进行中选择或文本字段中的输入。
本页面总结了根据您的状态被提升到的位置以及需要该状态的逻辑,可用于存储界面状态的 API。
界面逻辑
如果您的状态在界面中提升,无论是在可组合函数中还是在作用域限定为组合的普通状态持有者类中,您都可以使用rememberSaveable
在 Activity 和进程重建期间保留状态。
在以下代码段中,rememberSaveable
用于存储单个布尔型界面元素状态:
@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
是一个布尔变量,用于存储聊天气泡是折叠还是展开状态。
rememberSaveable
通过已保存实例状态机制将界面元素状态存储在Bundle
中。
它能够自动将原始类型存储到 Bundle 中。如果您的状态保存在非原始类型(如数据类)中,您可以使用不同的存储机制,例如使用 Parcelize
注解、使用 Compose API(如 listSaver
和 mapSaver
)或实现扩展 Compose 运行时 Saver
类的自定义 Saver 类。请参阅状态存储方式文档以了解有关这些方法的更多信息。
在以下代码段中,rememberLazyListState
Compose API 使用 rememberSaveable
存储 LazyListState
,后者包含 LazyColumn
或 LazyRow
的滚动状态。它使用 LazyListState.Saver
,这是一个能够存储和恢复滚动状态的自定义 Saver。在 Activity 或进程重建(例如,在更改设备方向等配置更改之后)之后,滚动状态会保留。
@Composable fun rememberLazyListState( initialFirstVisibleItemIndex: Int = 0, initialFirstVisibleItemScrollOffset: Int = 0 ): LazyListState { return rememberSaveable(saver = LazyListState.Saver) { LazyListState( initialFirstVisibleItemIndex, initialFirstVisibleItemScrollOffset ) } }
最佳实践
rememberSaveable
使用 Bundle
存储界面状态,该 Bundle
也被其他 API 共享写入,例如 Activity 中的 onSaveInstanceState()
调用。但是,此 Bundle
的大小是有限的,存储大对象可能会在运行时导致 TransactionTooLarge
异常。这在整个应用中都使用相同 Bundle
的单个 Activity
应用中可能特别成问题。
为了避免此类崩溃,您不应在 Bundle 中存储大型复杂对象或对象列表。
相反,请存储所需的最小状态(例如 ID 或键),并使用它们将更复杂的界面状态恢复委托给其他机制,例如持久存储。
这些设计选择取决于您应用的具体用例以及用户对其行为的预期。
验证状态恢复
您可以验证 Compose 元素中通过rememberSaveable
存储的状态在 Activity 或进程重新创建时是否正确恢复。有专门的 API 可以实现这一点,例如 StateRestorationTester
。请查阅测试文档以了解更多信息。
业务逻辑
如果您的界面元素状态被提升到 ViewModel
,因为业务逻辑需要它,您可以使用 ViewModel
的 API。
在 Android 应用中使用 ViewModel
的主要好处之一是它可以免费处理配置更改。当发生配置更改且 Activity 被销毁并重新创建时,提升到 ViewModel
的界面状态会保留在内存中。重新创建后,旧的 ViewModel
实例将附加到新的 Activity 实例。
但是,ViewModel
实例在系统发起的进程终止后无法存活。要使界面状态在此类事件中存活,请使用ViewModel 的 Saved State 模块,其中包含 SavedStateHandle
API。
最佳实践
SavedStateHandle
也使用 Bundle
机制来存储界面状态,因此您应该只用它来存储简单的界面元素状态。
屏幕界面状态是通过应用业务规则和访问应用中除界面之外的层而产生的,不应因其潜在的复杂性和大小而存储在 SavedStateHandle
中。您可以使用不同的机制来存储复杂或大型数据,例如本地持久存储。进程重新创建后,屏幕会使用存储在 SavedStateHandle
中(如果有)的已恢复瞬态状态重新创建,并且屏幕界面状态会再次从数据层生成。
SavedStateHandle
API
SavedStateHandle
提供了不同的 API 来存储界面元素状态,其中最值得注意的是:
Compose State |
saveable() |
---|---|
StateFlow |
getStateFlow() |
Compose State
使用 SavedStateHandle
的 saveable
API 将界面元素状态读取和写入为 MutableState
,以便它在 Activity 和进程重新创建后仍然存活,且代码设置最少。
saveable
API 开箱即用支持原始类型,并且像 rememberSaveable()
一样接收一个 stateSaver
参数以使用自定义 Saver。
在以下代码段中,message
存储用户输入的类型到 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) } ) }
有关使用 saveable
API 的更多信息,请参阅 SavedStateHandle
文档。
StateFlow
使用 getStateFlow()
存储界面元素状态,并将其作为流从 SavedStateHandle
中使用。 StateFlow
是只读的,并且 API 要求您指定一个键,以便您可以替换流以发出新值。使用您配置的键,您可以检索 StateFlow
并收集最新值。
在以下代码段中,savedFilterType
是一个 StateFlow
变量,它存储应用于聊天应用中聊天频道列表的过滤类型:
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 }
每次用户选择新的过滤类型时,都会调用 setFiltering
。这会在 SavedStateHandle
中以键 _CHANNEL_FILTER_SAVED_STATE_KEY_
存储新值。savedFilterType
是一个发出存储到该键的最新值的流。filteredChannels
订阅该流以执行频道过滤。
有关 getStateFlow()
API 的更多信息,请参阅 SavedStateHandle
文档。
摘要
下表总结了本节中介绍的 API,以及何时使用它们来保存界面状态:
事件 | 界面逻辑 | ViewModel 中的业务逻辑 |
---|---|---|
配置更改 | rememberSaveable |
自动 |
系统发起的进程终止 | rememberSaveable |
SavedStateHandle |
要使用的 API 取决于状态的持有位置以及所需的逻辑。对于用于界面逻辑的状态,请使用 rememberSaveable
。对于用于业务逻辑的状态,如果将其持有在 ViewModel
中,则使用 SavedStateHandle
保存。
您应该使用 Bundle API(rememberSaveable
和 SavedStateHandle
)来存储少量界面状态。这些数据是与其他存储机制一起将界面恢复到先前状态所需的最低限度。例如,如果您在 Bundle 中存储了用户正在查看的个人资料的 ID,则可以从数据层获取大量数据(例如个人资料详细信息)。
有关保存界面状态的不同方式的更多信息,请参阅通用保存界面状态文档以及架构指南的数据层页面。
为您推荐
- 注意:当 JavaScript 关闭时,会显示链接文本
- 状态提升位置
- 状态和 Jetpack Compose
- 列表和网格