状态和 Jetpack Compose

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

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

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

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

状态和组合

Compose 是声明式的,因此更新它的唯一方法是使用新的参数调用相同 composable。这些参数是 UI 状态的表示。每当状态更新时,都会发生重新组合。因此,诸如TextField之类的元素不会像在基于命令式 XML 的视图中那样自动更新。必须显式地告知 composable 新状态,才能使其相应地更新。

@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中的值。对于其他值,您可以传入自定义保存器对象。

其他受支持的状态类型

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

Compose 提供了从 Android 应用中常用的可观察类型创建State<T> 的函数。在使用这些集成之前,请添加如下所示的适当构件

  • FlowcollectAsStateWithLifecycle()

    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"
}
  • FlowcollectAsState()

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

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

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

  • LiveDataobserveAsState()

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

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

Kotlin

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

Groovy

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

Kotlin

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

Groovy

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

Kotlin

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

Groovy

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

有状态与无状态

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

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

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

状态提升

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

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

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

这样提升的状态具有一些重要的属性

  • 单一事实来源:通过移动状态而不是复制状态,我们确保只有一个事实来源。这有助于避免错误。
  • 封装:只有有状态组合函数才能修改其状态。它是完全内部的。
  • 可共享:提升的状态可以与多个组合函数共享。如果您想在不同的组合函数中读取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。通过遵循单向数据流,您可以将显示 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实例,因为重新创建它的代价很高。remember采用avatarRes作为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实现来判断键是否已更改并使存储的值失效。

在 recomposition 之外使用键存储状态

rememberSaveable API 是 remember 的一个包装器,它可以将数据存储在Bundle中。此 API 允许状态不仅能够在 recomposition 中生存,还能够在 Activity 重新创建和系统启动的进程死亡中生存。rememberSaveable接收input参数,其作用与remember接收keys参数相同。任何输入更改都会使缓存失效。函数下次 recompose 时,rememberSaveable将重新执行计算 lambda 块。

在下面的示例中,rememberSaveable存储userTypedQuery,直到typedQuery更改。

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

了解更多

要了解有关状态和 Jetpack Compose 的更多信息,请参阅以下其他资源。

示例

Codelabs

视频

博客