在 Compose 中保存 UI 状态

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

任何 Android 应用都可能因活动或进程重新创建而丢失其UI 状态。由于以下事件,可能会发生这种状态丢失

在这些事件之后保留状态对于获得良好的用户体验至关重要。选择要持久化的状态取决于您应用的独特用户流程。作为最佳实践,您至少应保留用户输入和与导航相关的状态。这包括列表的滚动位置、用户想要了解详细信息的项目的 ID、用户偏好的正在进行的选择或文本字段中的输入。

此页面总结了根据状态提升的位置和需要它的逻辑来存储 UI 状态的可用 API。

UI 逻辑

如果您的状态提升到 UI 中,无论是可组合函数还是作用域为 Composition 的普通状态持有者类,您都可以使用rememberSaveable来保留跨活动和进程重新创建的状态。

在以下代码片段中,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。在活动或进程重新创建(例如,在配置更改后,例如更改设备方向)后,滚动状态将被保留。

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

最佳实践

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

为了避免这种类型的崩溃,您不应在 bundle 中存储大型复杂对象或对象列表

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

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

验证状态恢复

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

业务逻辑

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

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

但是,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 参数来使用自定义保存器,就像 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 以 Flow 的形式使用它。 StateFlow 是只读的,此 API 需要您指定一个键,以便您可以替换 Flow 以发出新值。使用您配置的键,您可以检索 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 恢复到其先前状态所需的最小数据,以及其他存储机制。例如,如果将用户正在查看的个人资料 ID 存储在 bundle 中,则可以从数据层获取繁重的数据(如个人资料详细信息)。

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