状态与 Jetpack Compose

356 阅读6分钟

状态与 Jetpack Compose

应用中的状态是指任何随时间可能改变的值。这是一个非常宽泛的定义,涵盖了从 Room 数据库到类中的变量等所有内容。 所有的安卓应用都会向用户展示状态。以下是安卓应用中状态的一些示例:

  • 当无法建立网络连接时显示的 Snackbar。

  • 一篇博客文章以及相关的评论。

  • 用户点击按钮时出现的涟漪动画。

  • 用户可以在图像上绘制的贴纸。

Jetpack Compose 有助于明确在安卓应用中存储和使用状态的位置及方式。本指南重点关注状态与可组合函数之间的联系,以及 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 中的组合和重新组合机制决定的。

关键术语:

  • 组合:Jetpack Compose 执行可组合函数时构建 UI 的描述。

  • 初始组合:首次运行可组合函数创建组合。

  • 重新组合:当数据变化时重新运行可组合函数更新组合。

要了解更多关于初始组合和重新组合的信息,请参阅。《用 Compose 思考》.

可组合函数中的状态

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

注意: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 中使用诸如ArrayList<T>mutableListOf()等可变对象作为状态会导致用户在应用中看到不正确或过时的数据。像ArrayList或可变数据类这样不可观察的可变对象,Compose 无法观察到它们的变化,也不会在它们改变时触发重新组合。建议使用可观察的数据持有者,如State<List<T>>和不可变的listOf(),而不是使用不可观察的可变对象。

其他支持的状态类型

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

Compose 提供了从安卓应用中常用的可观察类型创建State<T>的函数。在使用这些集成之前,请按照以下说明添加相应的构件:

Flow

  • collectAsStateWithLifecycle():以生命周期感知的方式从Flow中收集值,允许应用节省应用资源。它表示 Compose State中最新发出的值。在安卓应用中收集Flow时,建议使用此 API。

注意:要了解更多关于在安卓中使用collectAsStateWithLifecycle() API 安全收集Flow的信息,请阅读相关博客文章。

build.gradle文件中需要以下依赖(应该是 2.6.0 - beta01 或更新版本):

dependencies {
     ...
      implementation("androidx.lifecycle:lifecycle-runtime-compose:2.8.5")
}
  • collectAsState():与collectAsStateWithLifecycle类似,因为它也从Flow中收集值并将其转换为 Compose State

对于平台无关的代码,使用collectAsState而不是仅适用于安卓的collectAsStateWithLifecyclecollectAsState不需要额外的依赖,因为它在compose-runtime中可用。

LiveData

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

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

dependencies {
     ...
      implementation("androidx.compose.runtime:runtime-livedata:1.7.3")
}
  • subscribeAsState():是将 RxJava2 的反应流(例如SingleObservableCompletable)转换为 Compose State的扩展函数。

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

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

RxJava3

  • subscribeAsState():是将 RxJava3 的反应流(例如SingleObservableCompletable)转换为 Compose State的扩展函数。

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

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

关键要点:Compose 会在读取State对象时自动重新组合。如果在 Compose 中使用其他可观察类型,如LiveData,在读取之前应该将其转换为State。确保在可组合函数中使用可组合函数扩展函数(如LiveData<T>.observeAsState())进行类型转换。

注意:你不限于这些集成。你可以为 Jetpack Compose 构建一个读取其他可观察类型的扩展函数。如果你的应用使用自定义的可观察类,可以使用produceState API 将其转换为产生State<T>

查看内置函数的实现以了解如何做到这一点,例如collectAsStateWithLifecycle。任何允许 Jetpack Compose 订阅其每一个变化的对象都可以转换为State<T>并在可组合函数中读取。

有状态与无状态

有状态可组合函数

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

无状态可组合函数

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

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

状态提升

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

  • value: T:要显示的当前值

  • onValueChange: (T) -> Unit:请求值改变的事件,其中T是提议的新值

然而,你不限于onValueChange。如果更适合可组合函数的特定事件,可以使用 lambda 表达式来定义它们。

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

  • 单一事实来源:通过移动状态而不是复制它,我们确保只有一个事实来源。这有助于避免错误。

  • 封装性:只有有状态的可组合函数可以修改其状态。它完全是内部的。

  • 可共享性:提升后的状态可以与多个可组合函数共享。如果想在不同的可组合函数中读取name,提升状态就可以做到。

  • 可拦截性:无状态可组合函数的调用者可以在改变状态之前决定忽略或修改事件。

  • 解耦性:无状态可组合函数的状态可以存储在任何地方。例如,现在可以将name移动到ViewModel中。

在示例中,我们从HelloContent中提取nameonValueChange,并将它们向上移动到调用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 中显示状态的可组合函数与应用中存储和改变状态的部分解耦。

关键要点:在提升状态时,有三条规则可以帮助你确定状态应该放在哪里:

  1. 状态应该提升到至少所有使用该状态(读取)的可组合函数的最低共同父级。

  2. 状态应该提升到至少它可能被改变(写入)的最高级别。

  3. 如果两个状态因相同事件而改变,它们应该一起提升。

你可以将状态提升得比这些规则要求的更高,但提升不足会使遵循单向数据流变得困难或不可能。

在 Compose 中恢复状态

rememberSaveable API 的行为类似于remember,因为它在重新组合以及使用保存的实例状态机制进行活动或进程重新创建(例如屏幕旋转)时保留状态。

注意:如果活动被用户完全关闭,rememberSaveable不会保留状态。例如,如果用户从最近使用的屏幕中将当前活动向上滑动关闭,它不会保留状态。

存储状态的方式

Parcelize

最简单的解决方案是给对象添加@Parcelize注解...

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
) { /*...*/ }

注意:如需了解更多关于普通状态持有者类的信息,请查看普通状态持有者类作为状态所有者的文档,或者架构指南中的状态持有者和 UI 状态文档。
Compose 使用类的equals实现来判断一个键是否改变,并使存储的值无效。
注意:乍一看,使用带键的remember可能看起来与使用其他 Compose API(如derivedStateOf)相似。查看the 《Jetpack Compose - 何时应该使用 derivedStateOf?》博客文章以了解差异。

超越重新组合的键存储状态

rememberSaveable API 是remember的一个包装器,它可以将数据存储在Bundle中。这个 API 允许状态不仅在重新组合时存活,还能在活动重新创建和系统发起的进程死亡时存活。rememberSaveable接收input参数,其目的与remember接收keys相同。当任何输入改变时,缓存会无效。下次函数重新组合时,rememberSaveable会重新执行计算 lambda 块。

注意:API 命名存在差异需要注意。在remember API 中,使用参数名keys,而在rememberSaveable中,使用inputs用于相同目的。如果这些参数中的任何一个改变,缓存的值会无效。
在下面的示例中,rememberSaveable存储userTypedQuery直到typedQuery改变: