在 Compose 中保存界面状态

根据要将状态提升到什么位置和所需的逻辑,您可以使用不同的 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)
    }
}

图 1. 聊天消息气泡在点按后展开和收起。

showDetails 是一个布尔值变量,用于存储聊天气泡是收起还是展开。

rememberSaveable 通过保存的实例状态机制将界面元素状态存储在 Bundle 中。

它能够自动将基元类型存储到 Bundle 中。如果您的状态并非以基元类型存储(如数据类),则可以使用不同的存储机制,例如使用 Parcelize 注解、使用 listSavermapSaver 等 Compose API,或实现会扩展 Compose 运行时 Saver 类的自定义 Saver 类。如需详细了解这些方法,请参阅存储状态的方式文档。

在以下代码段中,rememberLazyListState Compose API 会使用 rememberSaveable 存储 LazyListState,其中包含 LazyColumnLazyRow 的滚动状态。该 API 使用 LazyListState.Saver,这是能够存储和恢复滚动状态的自定义 Saver。在重新创建 activity 或进程后(例如,在设备屏幕方向等配置发生更改后),滚动状态将得以保留。

@Composable
fun rememberLazyListState(
    initialFirstVisibleItemIndex: Int = 0,
    initialFirstVisibleItemScrollOffset: Int = 0
): LazyListState {
    return rememberSaveable(saver = LazyListState.Saver) {
        LazyListState(
            initialFirstVisibleItemIndex, initialFirstVisibleItemScrollOffset
        )
    }
}

最佳实践

rememberSaveable 使用 Bundle 存储界面状态,该状态由同样向其写入数据的其他 API(例如 activity 中的 onSaveInstanceState() 调用)共享。不过,此 Bundle 的大小有限,如果存储大型对象,可能会导致运行时出现 TransactionTooLarge 异常。对于在整个应用中使用同一 Bundle 的单个 Activity 应用,此问题尤其明显。

为避免发生此类崩溃,您不应在 Bundle 中存储大型复杂对象或对象列表。

您应存储最少的必需状态数据(例如 ID 或键),并使用此类数据将恢复更复杂的界面状态委托给其他机制(例如永久性存储)。

这些设计选项取决于应用的具体用例以及用户对应用行为方式的预期。

验证状态恢复

您可以验证重新创建 activity 或进程后,Compose 元素中使用 rememberSaveable 存储的状态是否已正确恢复。您可以借助特定 API 来实现此目的,例如 StateRestorationTester。如需了解详情,请参阅此测试文档。

业务逻辑

如果由于业务逻辑的需要,界面元素状态被提升到 ViewModel,您就可以使用 ViewModel 的 API。

在 Android 应用中使用 ViewModel 的一个主要优势是,无需开销即可处理配置更改。当发生配置更改,并且 activity 被销毁并重新创建时,提升到 ViewModel 的界面状态会保留在内存中。重新创建完成后,旧的 ViewModel 实例会附加到新的 activity 实例。

但是,ViewModel 实例在系统发起的进程终止后将失效。如需让界面状态保留下来,请使用适用于 ViewModel 的“已保存状态”模块,其中包含 SavedStateHandle API。

最佳实践

SavedStateHandle 还使用 Bundle 机制存储界面元素状态,因此您应只使用它来存储简单的界面元素状态

屏幕界面状态是通过应用业务规则并访问应用的层(而非界面)生成的,可能非常复杂且庞大,因此不应存储在 SavedStateHandle 中。您可以使用不同的机制来存储复杂或大型数据,例如本地永久性存储。重新创建进程后,系统会使用存储在 SavedStateHandle 中的已恢复瞬时状态(如有)重新创建屏幕,并从数据层再次生成屏幕界面状态。

SavedStateHandle API

SavedStateHandle 具有用于存储界面元素状态的不同 API,最值得注意的是:

Compose State saveable()
StateFlow getStateFlow()

Compose State

使用 SavedStateHandlesaveable API 以 MutableState 形式读取和写入界面元素状态,以便只需极少的代码设置就能在重新创建 activity 和进程后继续保留状态。

saveable API 开箱就支持基元类型,并会收到 stateSaver 参数,以便使用自定义 Saver(就像 rememberSaveable() 一样)。

在以下代码段中,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 以及何时使用其中每个 API 保存界面状态:

活动 界面逻辑 ViewModel 中的业务逻辑
配置更改 rememberSaveable 自动
系统发起的进程终止 rememberSaveable SavedStateHandle

要使用哪个 API 取决于状态存储在什么位置及所需的逻辑。对于界面逻辑中使用的状态,请使用 rememberSaveable。对于业务逻辑中使用的状态,如果将其存储在 ViewModel 中,请使用 SavedStateHandle 进行保存。

您应该使用 Bundle API(rememberSaveableSavedStateHandle)存储少量界面状态。此数据是使用其他存储机制将界面恢复到之前的状态所需的最少数据。例如,如果要将用户刚才查看的配置文件的 ID 存储在 Bundle 中,则可以从数据层提取大量数据(如配置文件详细信息)。

如需详细了解保存界面状态的不同方法,请参阅常规保存界面状态文档和架构指南的数据层页面。