在 Compose 中保存 UI 状态

根据状态提升到的位置和所需的逻辑,您可以使用不同的 API 来存储和恢复您的UI 状态。每个应用都使用 API 的组合来最好地实现这一点。

任何 Android 应用都可能由于 Activity 或进程重建而丢失其UI 状态。状态丢失可能由以下事件引起

在这些事件发生后保留状态对于提供良好的用户体验至关重要。选择要持久化的状态取决于应用的独特用户流程。最佳实践是至少保留用户输入和与导航相关的状态。例如,列表的滚动位置、用户想要查看详细信息的项目的 ID、用户正在进行的偏好选择或文本字段中的输入。

此页面总结了根据状态提升到的位置以及需要状态的逻辑,可用于存储 UI 状态的 API。

UI 逻辑

如果您的状态提升到 UI 中,无论是可组合函数还是作用域限定到 Composition 的普通状态持有者类,您都可以使用rememberSaveable在 Activity 和进程重建之间保留状态。

在以下代码片段中,rememberSaveable 用于存储单个布尔型 UI 元素状态

@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 通过已保存实例状态机制将UI 元素状态存储在Bundle中。

它能够自动将基本类型存储到 Bundle 中。如果您的状态保存在非基本类型中(例如数据类),则可以使用不同的存储机制,例如使用Parcelize注解,使用 Compose API(如listSavermapSaver),或实现扩展 Compose 运行时Saver类的自定义 Saver 类。请参阅存储状态的方法文档,以了解有关这些方法的更多信息。

在以下代码片段中,rememberLazyListState Compose API 使用rememberSaveable存储LazyListState,它包含LazyColumnLazyRow的滚动状态。它使用LazyListState.Saver,这是一个能够存储和恢复滚动状态的自定义 Saver。在 Activity 或进程重建后(例如,在配置更改(如更改设备方向)后),滚动状态将被保留。

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

最佳实践

rememberSaveable 使用Bundle存储 UI 状态,该状态由其他也写入它的 API 共享,例如 Activity 中的onSaveInstanceState()调用。但是,此Bundle的大小是有限制的,存储大型对象可能会导致运行时出现TransactionTooLarge异常。这在单个Activity应用中尤其成问题,因为整个应用都在使用同一个Bundle

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

相反,应存储所需的最小状态(如 ID 或键),并使用它们将恢复更复杂的 UI 状态委派给其他机制,例如持久性存储

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

验证状态恢复

您可以验证在 Activity 或进程重新创建时,Compose 元素中使用rememberSaveable存储的状态是否已正确恢复。有一些特定的 API 可以实现此目的,例如StateRestorationTester。请查看测试文档以了解更多信息。

业务逻辑

如果您的UI 元素状态提升到ViewModel,因为业务逻辑需要它,您可以使用ViewModel的 API。

在 Android 应用中使用ViewModel的主要优势之一是它可以免费处理配置更改。当发生配置更改并且 Activity 被销毁并重新创建时,提升到ViewModel的 UI 状态将保留在内存中。重建后,旧的ViewModel实例将附加到新的 Activity 实例。

但是,ViewModel实例无法在系统触发的进程终止后继续存在。若要使 UI 状态在这种情况后继续存在,请使用ViewModel 的 Saved State 模块,其中包含SavedStateHandle API。

最佳实践

SavedStateHandle也使用Bundle机制存储 UI 状态,因此您应该只使用它来存储简单的UI 元素状态

通过应用业务规则并访问除 UI 之外的应用层产生的屏幕 UI 状态,不应存储在SavedStateHandle中,因为它可能很复杂且很大。您可以使用不同的机制来存储复杂或大型数据,例如本地持久性存储。进程重建后,屏幕将使用存储在SavedStateHandle(如果存在)中的已恢复瞬态状态重新创建,然后从数据层重新生成屏幕 UI 状态。

SavedStateHandle API

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

Compose State saveable()
StateFlow getStateFlow()

Compose State

使用SavedStateHandlesaveable API 将 UI 元素状态读写为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()存储 UI 元素状态并将其作为流从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 是一个 Flow,它会发出存储到该键的最新值。 filteredChannels 订阅了该 Flow 以执行频道过滤。

有关 getStateFlow() API 的更多信息,请参阅 SavedStateHandle 文档。

摘要

下表总结了本节中介绍的 API,以及何时使用每个 API 来保存 UI 状态。

事件 UI 逻辑 ViewModel 中的业务逻辑
配置更改 rememberSaveable 自动
系统发起的进程死亡 rememberSaveable SavedStateHandle

要使用的 API 取决于状态的保存位置及其所需的逻辑。对于在 UI 逻辑 中使用的状态,请使用 rememberSaveable。对于在 业务逻辑 中使用的状态,如果将其保存在 ViewModel 中,请使用 SavedStateHandle 保存它。

您应该使用 bundle API(rememberSaveableSavedStateHandle)来存储少量 UI 状态。此数据是将 UI 恢复到其先前状态所需的最小必要数据,以及其他存储机制。例如,如果在 bundle 中存储用户正在查看的个人资料 ID,则可以从数据层获取重量级数据(如个人资料详细信息)。

有关保存 UI 状态的不同方法的更多信息,请参阅通用 保存 UI 状态文档 和架构指南的 数据层 页面。