状态和 Jetpack Compose

应用中的状态是指随时间变化的任何值。这是一个非常宽泛的定义,涵盖了从 Room 数据库到类中的变量的一切。

所有 Android 应用都会向用户显示状态。Android 应用中状态的几个示例:

  • 当无法建立网络连接时显示的 Snackbar。
  • 一篇博文和相关的评论。
  • 用户点击按钮时播放的 Ripple 动画。
  • 用户可以在图片上绘制的贴纸。

Jetpack Compose 可帮助您明确地说明在 Android 应用中存储和使用状态的位置和方式。本指南重点介绍状态与可组合项之间的连接,以及 Jetpack Compose 提供的可更轻松处理状态的 API。

状态和组合

Compose 是声明性的,因此更新它的唯一方法是使用新参数调用相同的可组合项。这些参数是界面状态的表示形式。每当状态更新时,都会发生重组。因此,TextField 等组件不会像在命令式 XML 视图中那样自动更新。可组合项必须明确地被告知新状态,才能进行相应的更新。

@Composable
private fun HelloContent() {
    Column(modifier = Modifier.padding(16.dp)) {
        Text(
            text = "Hello!",
            modifier = Modifier.padding(bottom = 8.dp),
            style = MaterialTheme.typography.bodyMedium
        )
        OutlinedTextField(
            value = "",
            onValueChange = { },
            label = { Text("Name") }
        )
    }
}

如果您运行此代码并尝试输入文本,会发现没有任何反应。这是因为 TextField 不会自动更新,它会在其 value 参数更改时更新。这归因于 Compose 中组合和重组的工作方式。

要详细了解初始组合和重组,请参阅Compose 思维

可组合项中的状态

可组合函数可以使用 remember API 将对象存储在内存中。由 remember 计算的值会在初始组合期间存储在组合中,并在重组期间返回存储的值。remember 可用于存储可变对象和不可变对象。

mutableStateOf 会创建一个可观察的 MutableState<T>,这是一种与 Compose 运行时集成的可观察类型。

interface MutableState<T> : State<T> {
    override var value: T
}

value 的任何更改都会调度读取 value 的任何可组合函数的重组。

在可组合项中声明 MutableState 对象的几种方法:

  • val mutableState = remember { mutableStateOf(default) }
  • var value by remember { mutableStateOf(default) }
  • val (value, setValue) = remember { mutableStateOf(default) }

这些声明是等效的,并作为不同状态用法的语法糖提供。您应该选择在您正在编写的可组合项中生成最易读代码的声明方式。

使用by委托语法需要以下导入:

import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue

您可以将记住的值用作其他可组合项的参数,甚至用作语句中的逻辑来更改显示哪些可组合项。例如,如果姓名为空时不想显示问候语,可以在 if 语句中使用状态:

@Composable
fun HelloContent() {
    Column(modifier = Modifier.padding(16.dp)) {
        var name by remember { mutableStateOf("") }
        if (name.isNotEmpty()) {
            Text(
                text = "Hello, $name!",
                modifier = Modifier.padding(bottom = 8.dp),
                style = MaterialTheme.typography.bodyMedium
            )
        }
        OutlinedTextField(
            value = name,
            onValueChange = { name = it },
            label = { Text("Name") }
        )
    }
}

虽然 remember 可帮助您在重组时保留状态,但状态不会在配置更改时保留。为此,您必须使用 rememberSaveablerememberSaveable 会自动保存可以存储在 Bundle 中的任何值。对于其他值,您可以传入自定义 Saver 对象。

其他受支持的状态类型

Compose 不要求您使用 MutableState<T> 来持有状态;它支持其他可观察类型。在 Compose 中读取其他可观察类型之前,您必须将其转换为 State<T>,以便可组合项在状态更改时自动重组。

Compose 附带了一些函数,可从 Android 应用中使用的常见可观察类型创建 State<T>。在使用这些集成之前,请按以下说明添加相应的 工件

  • Flow: collectAsStateWithLifecycle()

    collectAsStateWithLifecycle() 以生命周期感知的方式收集来自 Flow 的值,使您的应用能够节省应用资源。它表示 Compose State 的最新发出值。建议使用此 API 在 Android 应用中收集流。

    以下 依赖项build.gradle 文件中是必需的(版本应为 2.6.0-beta01 或更高版本):

Kotlin

dependencies {
      ...
      implementation("androidx.lifecycle:lifecycle-runtime-compose:2.8.7")
}

Groovy

dependencies {
      ...
      implementation "androidx.lifecycle:lifecycle-runtime-compose:2.8.7"
}
  • Flow: collectAsState()

    collectAsState 类似于 collectAsStateWithLifecycle,因为它也从 Flow 收集值并将其转换为 Compose State

    对于平台无关的代码,请使用 collectAsState,而不是仅适用于 Android 的 collectAsStateWithLifecycle

    collectAsState 不需要额外的依赖项,因为它在 compose-runtime 中可用。

  • LiveData: observeAsState()

    observeAsState() 开始观察此 LiveData 并使用 State 表示其值。

    以下 依赖项build.gradle 文件中是必需的:

Kotlin

dependencies {
      ...
      implementation("androidx.compose.runtime:runtime-livedata:1.8.1")
}

Groovy

dependencies {
      ...
      implementation "androidx.compose.runtime:runtime-livedata:1.8.1"
}

Kotlin

dependencies {
      ...
      implementation("androidx.compose.runtime:runtime-rxjava2:1.8.1")
}

Groovy

dependencies {
      ...
      implementation "androidx.compose.runtime:runtime-rxjava2:1.8.1"
}

Kotlin

dependencies {
      ...
      implementation("androidx.compose.runtime:runtime-rxjava3:1.8.1")
}

Groovy

dependencies {
      ...
      implementation "androidx.compose.runtime:runtime-rxjava3:1.8.1"
}

有状态与无状态

使用 remember 存储对象的可组合项会创建内部状态,使该可组合项成为有状态的HelloContent 是有状态可组合项的一个示例,因为它在内部持有和修改其 name 状态。这在调用方无需控制状态并且无需自行管理状态即可使用状态的情况下非常有用。但是,具有内部状态的可组合项往往可重用性较低,且更难测试。

无状态可组合项是不持有任何状态的可组合项。实现无状态的一种简单方法是使用状态提升

在开发可重用可组合项时,您通常会希望公开同一可组合项的有状态版本和无状态版本。有状态版本便于不关心状态的调用方使用,而无状态版本对于需要控制或提升状态的调用方而言是必需的。

状态提升

Compose 中的状态提升是一种将状态移至可组合项的调用方,以使可组合项变为无状态的模式。Jetpack Compose 中状态提升的通用模式是使用两个参数替换状态变量:

  • value: T 要显示的当前值
  • onValueChange: (T) -> Unit 请求更改值的事件,其中 T 是提议的新值

但是,您不仅限于 onValueChange。如果更具体的事件适合可组合项,您应该使用 lambda 定义它们。

以这种方式提升的状态具有一些重要属性:

  • 单一可信来源: 通过移动状态而不是复制状态,我们确保只有一个可信来源。这有助于避免 Bug。
  • 封装: 只有有状态可组合项可以修改其状态。它完全是内部的。
  • 可共享: 提升的状态可以与多个可组合项共享。如果您想在不同的可组合项中读取 name,提升将允许您这样做。
  • 可拦截: 无状态可组合项的调用方可以在更改状态之前决定忽略或修改事件。
  • 解耦: 无状态可组合项的状态可以存储在任何位置。例如,现在可以将 name 移到 ViewModel 中。

在本示例中,您将 nameonValueChangeHelloContent 中提取出来,并将它们向上移动到调用 HelloContentHelloScreen 可组合项中。

@Composable
fun HelloScreen() {
    var name by rememberSaveable { mutableStateOf("") }

    HelloContent(name = name, onNameChange = { name = it })
}

@Composable
fun HelloContent(name: String, onNameChange: (String) -> Unit) {
    Column(modifier = Modifier.padding(16.dp)) {
        Text(
            text = "Hello, $name",
            modifier = Modifier.padding(bottom = 8.dp),
            style = MaterialTheme.typography.bodyMedium
        )
        OutlinedTextField(value = name, onValueChange = onNameChange, label = { Text("Name") })
    }
}

通过将状态从 HelloContent 中提升出来,可以更容易地推断可组合项,在不同情况下重用它,并进行测试。HelloContent 与其状态的存储方式解耦。解耦意味着如果您修改或替换 HelloScreen,则无需更改 HelloContent 的实现方式。

状态向下流动、事件向上流动的模式称为单向数据流。在此示例中,状态从 HelloScreen 流向 HelloContent,事件从 HelloContent 流向 HelloScreen。通过遵循单向数据流,您可以将界面中显示状态的可组合项与应用中存储和更改状态的部分解耦。

如需了解详情,请参阅状态提升的位置页面。

在 Compose 中恢复状态

rememberSaveable API 的行为与 remember 类似,因为它会在重组时以及使用保存的实例状态机制重新创建 Activity 或进程时保留状态。例如,当屏幕旋转时,就会发生这种情况。

存储状态的方法

所有添加到 Bundle 中的数据类型都会自动保存。如果您想保存无法添加到 Bundle 中的内容,则有以下几种选择。

Parcelize

最简单的解决方案是将 @Parcelize 注解添加到对象中。该对象变为 parcelable,并且可以打包。例如,此代码会创建一个 parcelable City 数据类型并将其保存到状态中。

@Parcelize
data class City(val name: String, val country: String) : Parcelable

@Composable
fun CityScreen() {
    var selectedCity = rememberSaveable {
        mutableStateOf(City("Madrid", "Spain"))
    }
}

MapSaver

如果因某种原因 @Parcelize 不适合,您可以使用 mapSaver 定义自己的规则,将对象转换为系统可以保存到 Bundle 的一组值。

data class City(val name: String, val country: String)

val CitySaver = run {
    val nameKey = "Name"
    val countryKey = "Country"
    mapSaver(
        save = { mapOf(nameKey to it.name, countryKey to it.country) },
        restore = { City(it[nameKey] as String, it[countryKey] as String) }
    )
}

@Composable
fun CityScreen() {
    var selectedCity = rememberSaveable(stateSaver = CitySaver) {
        mutableStateOf(City("Madrid", "Spain"))
    }
}

ListSaver

为避免需要为映射定义键,您还可以使用 listSaver 并将其索引用作键:

data class City(val name: String, val country: String)

val CitySaver = listSaver<City, Any>(
    save = { listOf(it.name, it.country) },
    restore = { City(it[0] as String, it[1] as String) }
)

@Composable
fun CityScreen() {
    var selectedCity = rememberSaveable(stateSaver = CitySaver) {
        mutableStateOf(City("Madrid", "Spain"))
    }
}

Compose 中的状态持有者

简单的状态提升可以在可组合函数本身中进行管理。但是,如果需要跟踪的状态量增加,或者可组合函数中需要执行的逻辑出现,则最好将逻辑和状态职责委托给其他类:状态持有者

如需了解详情,请参阅Compose 中的状态提升文档,或者更宽泛地讲,架构指南中的状态持有者和界面状态页面。

当键更改时重新触发 remember 计算

remember API 经常与 MutableState 一起使用:

var name by remember { mutableStateOf("") }

此处,使用 remember 函数可使 MutableState 值在重组后继续存在。

通常,remember 接受一个 calculation lambda 参数。当 remember 首次运行时,它会调用 calculation lambda 并存储其结果。在重组期间,remember 会返回上次存储的值。

除了缓存状态之外,您还可以使用 remember 在组合中存储任何初始化或计算开销较大的对象或操作结果。您可能不希望在每次重组时重复此计算。一个示例是创建 ShaderBrush 对象,这是一项开销较大的操作:

val brush = remember {
    ShaderBrush(
        BitmapShader(
            ImageBitmap.imageResource(res, avatarRes).asAndroidBitmap(),
            Shader.TileMode.REPEAT,
            Shader.TileMode.REPEAT
        )
    )
}

remember 会存储值,直到其离开组合为止。但是,有一种方法可以使缓存值失效。remember API 还接受 keykeys 参数。如果其中任何键发生更改,下次函数重组时remember 会使缓存失效并再次执行计算 lambda 块。此机制让您可以控制组合中对象的生命周期。计算结果在输入发生更改之前一直有效,而不是直到记住的值离开组合为止。

以下示例展示了此机制的工作原理。

在此代码段中,会创建 ShaderBrush 并将其用作 Box 可组合项的背景画笔。如前所述,remember 会存储 ShaderBrush 实例,因为重新创建它的开销很大。rememberavatarRes 作为 key1 参数,该参数是选定的背景图片。如果 avatarRes 发生更改,画笔会使用新图片重组,并重新应用于 Box。当用户从选择器中选择另一张图片作为背景时,可能会发生这种情况。

@Composable
private fun BackgroundBanner(
    @DrawableRes avatarRes: Int,
    modifier: Modifier = Modifier,
    res: Resources = LocalContext.current.resources
) {
    val brush = remember(key1 = avatarRes) {
        ShaderBrush(
            BitmapShader(
                ImageBitmap.imageResource(res, avatarRes).asAndroidBitmap(),
                Shader.TileMode.REPEAT,
                Shader.TileMode.REPEAT
            )
        )
    }

    Box(
        modifier = modifier.background(brush)
    ) {
        /* ... */
    }
}

在下一个代码段中,状态被提升到普通状态持有者类 MyAppState。它公开了一个 rememberMyAppState 函数,用于使用 remember 初始化该类的实例。公开此类函数以创建在重组后仍存在的实例是 Compose 中的一种常见模式。rememberMyAppState 函数会接收 windowSizeClass,它用作 rememberkey 参数。如果此参数发生更改,应用需要使用最新值重新创建普通状态持有者类。例如,如果用户旋转设备,可能会发生这种情况。

@Composable
private fun rememberMyAppState(
    windowSizeClass: WindowSizeClass
): MyAppState {
    return remember(windowSizeClass) {
        MyAppState(windowSizeClass)
    }
}

@Stable
class MyAppState(
    private val windowSizeClass: WindowSizeClass
) { /* ... */ }

Compose 使用类的 equals 实现来判断键是否已更改并使存储的值失效。

在重组之外使用键存储状态

rememberSaveable API 是 remember 的一个封装,可以将数据存储在 Bundle 中。此 API 允许状态不仅在重组时继续存在,还可以在 Activity 重新创建和系统发起的进程终止时继续存在。rememberSaveable 接收 input 参数,其用途与 remember 接收 keys 的用途相同。当任何输入更改时,缓存会失效。下次函数重组时,rememberSaveable 会重新执行计算 lambda 块。

在以下示例中,rememberSaveable 会存储 userTypedQuery,直到 typedQuery 更改:

var userTypedQuery by rememberSaveable(typedQuery, stateSaver = TextFieldValue.Saver) {
    mutableStateOf(
        TextFieldValue(text = typedQuery, selection = TextRange(typedQuery.length))
    )
}

了解详情

要详细了解状态和 Jetpack Compose,请查阅以下其他资源。

示例

Codelab

视频

博客