状态和 Jetpack Compose

应用程序中的状态是指任何可能随时间变化的值。这是一个非常宽泛的定义,它涵盖了从 Room 数据库到类中的变量的所有内容。

所有 Android 应用程序都向用户显示状态。以下是一些 Android 应用程序中状态的示例

  • 当无法建立网络连接时显示的 Snackbar。
  • 博客文章及其关联评论。
  • 用户点击按钮时播放的按钮上的涟漪动画。
  • 用户可以在图像上绘制的贴纸。

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

状态和组合

Compose 是声明式的,因此更新它的唯一方法是使用新的参数调用同一个可组合函数。这些参数是 UI 状态的表示。每当状态更新时,就会发生重新组合。因此,像 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 计算的值将存储在 Composition 中,并在重新组合期间返回存储的值。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 中保存的任何值。对于其他值,您可以传入自定义保存器对象。

其他支持的状态类型

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.5")
}

Groovy

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

    collectAsStatecollectAsStateWithLifecycle 类似,因为它也会从 Flow 收集值并将其转换为 Compose State

    对于平台无关代码,请使用 collectAsState 而不是 collectAsStateWithLifecycle(仅限 Android)。

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

  • LiveData: observeAsState()

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

    build.gradle 文件中需要以下 依赖项

Kotlin

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

Groovy

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

Kotlin

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

Groovy

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

Kotlin

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

Groovy

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

有状态与无状态

使用 remember 来存储对象的可组合函数会创建内部状态,从而使可组合函数成为有状态的。HelloContent 是有状态可组合函数的一个示例,因为它在内部保存并修改其 name 状态。这在调用者不需要控制状态并可以使用状态而无需自行管理状态的情况下非常有用。但是,具有内部状态的可组合函数往往可重用性较差,而且更难测试。

无状态可组合函数是指不保存任何状态的可组合函数。实现无状态的一种简单方法是使用 状态提升

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

状态提升

Compose 中的状态提升是一种将状态移动到可组合函数调用者的模式,以使可组合函数成为无状态的。Jetpack Compose 中状态提升的一般模式是用两个参数替换状态变量

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

但是,您并不局限于 onValueChange。如果更具体的事件适合可组合函数,则应使用 lambda 定义它们。

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

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

在示例案例中,您将 nameonValueChangeHelloContent 中提取出来,并将它们向上移动到树中的 HelloScreen 可组合函数,该函数调用 HelloContent

@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。通过遵循单向数据流,您可以将显示 UI 中状态的可组合函数与存储和更改状态的应用程序部分解耦。

有关详细信息,请参阅 在何处提升状态 页面。

在 Compose 中恢复状态

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

存储状态的方法

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

Parcelize

最简单的解决方案是将 @Parcelize 注解添加到对象中。该对象将变为可打包的,并且可以打包。例如,此代码创建了一个可打包的 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 中的状态提升 文档,或者更一般地说,请参阅架构指南中的 状态持有者和 UI 状态 页面。

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

remember API 通常与 MutableState 一起使用

var name by remember { mutableStateOf("") }

在这里,使用 remember 函数使 MutableState 值能够在重组中存活下来。

一般来说,remember 接收一个 calculation lambda 参数。当第一次运行 remember 时,它会调用 calculation lambda 并存储其结果。在重组期间,remember 返回最后存储的值。

除了缓存状态外,您还可以使用 remember 在 Composition 中存储任何对象或操作结果,这些对象或操作结果的初始化或计算成本很高。您可能不想在每次重组时重复此计算。一个例子是创建此 ShaderBrush 对象,这是一个代价高昂的操作

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

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

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

在此代码段中,创建了 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 允许状态不仅能够在重组中存活下来,还能在活动重建和系统启动的进程死亡中存活下来。 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 的更多信息,请参阅以下其他资源。

示例

Codelabs

视频

博客