应用中的状态是指可以随时间变化的任何值。这是一个非常宽泛的定义,从 Room 数据库到类的变量,全部涵盖在内。
所有 Android 应用都会向用户显示状态。下面是 Android 应用中的一些状态示例:
- 在无法建立网络连接时显示的信息提示控件。
- 博文和相关评论。
- 在用户点击按钮时播放的涟漪效果。
- 用户可以在图片上绘制的贴纸。
Jetpack Compose 可帮助您明确状态在 Android 应用中的存储位置和使用方式。本指南将重点介绍状态与可组合项之间的关联,以及 Jetpack Compose 提供了哪些 API 来助您更轻松地处理状态。
状态和组合
由于 Compose 是声明式工具集,因此更新它的唯一方法是通过新参数调用同一可组合项。这些参数是界面状态的表现形式。每当状态更新时,都会发生重组。因此,TextField
不会像在基于 XML 的命令式视图中那样自动更新。可组合项必须明确获知新状态,才能相应地进行更新。
@Composable
fun HelloContent() {
Column(modifier = Modifier.padding(16.dp)) {
Text(
text = "Hello!",
modifier = Modifier.padding(bottom = 8.dp),
style = MaterialTheme.typography.h5
)
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
的所有可组合函数安排重组。对于 ExpandingCard
,每当 expanded
发生变化时,都会导致 ExpandingCard
重组。
在可组合项中声明 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.h5
)
}
OutlinedTextField(
value = name,
onValueChange = { name = it },
label = { Text("Name") }
)
}
}
虽然 remember
可帮助您在重组后保持状态,但不会帮助您在配置更改后保持状态。为此,您必须使用 rememberSaveable
。rememberSaveable
会自动保存可保存在 Bundle
中的任何值。对于其他值,您可以将其传入自定义 Saver 对象。
其他受支持的状态类型
Jetpack Compose 并不要求您使用 MutableState<T>
存储状态。Jetpack Compose 支持其他可观察类型。在 Jetpack Compose 中读取其他可观察类型之前,您必须将其转换为 State<T>
,以便 Jetpack Compose 可以在状态发生变化后自动重组。
Compose 附带一些可以根据 Android 应用中使用的常见可观察类型创建 State<T>
的函数。在使用这些集成之前,请先添加适当的工件,如下所述:
Flow
:collectAsStateWithLifecycle()
collectAsStateWithLifecycle()
会以生命周期感知型方式从Flow
收集值,以便应用能够保存不需要的应用资源。它通过 ComposeState
表示最新发出的值。请将此 API 作为在 Android 应用中收集数据流的建议方法。build.gradle
文件中需要以下依赖项(应为 2.6.0-beta01 或更高版本):
Kotlin
dependencies {
...
implementation("androidx.lifecycle:lifecycle-runtime-compose:2.6.0-beta01")
}
Groovy
dependencies {
...
implementation "androidx.lifecycle:lifecycle-runtime-compose:2.6.0-beta01"
}
-
collectAsState
与collectAsStateWithLifecycle
类似,因为它也会从Flow
收集值并将其转换为 ComposeState
。请为平台通用代码使用
collectAsState
,而不要使用仅适用于 Android 的collectAsStateWithLifecycle
。collectAsState
可在compose-runtime
中使用,因此不需要其他依赖项。 -
observeAsState()
会开始观察此LiveData
,并通过State
表示其值。build.gradle
文件中需要以下依赖项:
Kotlin
dependencies {
...
implementation("androidx.compose.runtime:runtime-livedata:1.3.2")
}
Groovy
dependencies {
...
implementation "androidx.compose.runtime:runtime-livedata:1.3.2"
}
-
subscribeAsState()
是扩展函数,可将 RxJava2 的响应式流(例如Single
、Observable
、Completable
)转换成 ComposeState
。build.gradle
文件中需要以下依赖项:
Kotlin
dependencies {
...
implementation("androidx.compose.runtime:runtime-rxjava2:1.3.2")
}
Groovy
dependencies {
...
implementation "androidx.compose.runtime:runtime-rxjava2:1.3.2"
}
-
subscribeAsState()
是扩展函数,可将 RxJava3 的响应式流(例如Single
、Observable
、Completable
)转换成 ComposeState
。build.gradle
文件中需要以下依赖项:
Kotlin
dependencies {
...
implementation("androidx.compose.runtime:runtime-rxjava3:1.3.2")
}
Groovy
dependencies {
...
implementation "androidx.compose.runtime:runtime-rxjava3:1.3.2"
}
有状态与无状态
使用 remember
存储对象的可组合项会创建内部状态,使该可组合项有状态。HelloContent
就是一个有状态可组合项的示例,因为它会在内部保持和修改自己的 name
状态。在调用方不需要控制状态,并且不必自行管理状态便可使用状态的情况下,“有状态”会非常有用。但是,具有内部状态的可组合项往往不易重复使用,也更难测试。
无状态可组合项是指不保持任何状态的可组合项。实现无状态的一种简单方法是使用状态提升。
在开发可重复使用的可组合项时,您通常想要同时提供同一可组合项的有状态和无状态版本。有状态版本对于不关心状态的调用方来说很方便,而无状态版本对于需要控制或提升状态的调用方来说是必要的。
状态提升
Compose 中的状态提升,是一种将状态移至可组合项的调用方,使可组合项变成无状态的模式。Jetpack Compose 中的常规状态提升模式是将状态变量替换为两个参数:
value: T
:要显示的当前值onValueChange: (T) -> Unit
:请求更改值的事件,其中T
是建议的新值
不过,并不局限于 onValueChange
。如果更具体的事件适合可组合项,您应使用 lambda 定义这些事件,就像使用 onExpand
和 onCollapse
定义适合 ExpandingCard
的事件一样。
以这种方式提升的状态具有一些重要的属性:
- 单一可信来源:通过移动状态,而不是复制状态,我们可确保只有一个可信来源。这有助于避免 bug。
- 封装:只有有状态可组合项能够修改其状态。完全是在内部操作。
- 可共享:可与多个可组合项共享提升的状态。如果您想在另一个可组合项中读取
name
,可以通过变量提升来做到这一点。 - 可拦截:无状态可组合项的调用方可以在更改状态之前决定忽略或修改事件。
- 解耦:无状态
ExpandingCard
的状态可以存储在任何位置。例如,现在可以将name
移入ViewModel
。
在本示例中,您从 HelloContent
中提取 name
和 onValueChange
,并按照可组合项的树结构将它们移至可调用 HelloContent
的 HelloScreen
可组合项中。
@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.h5
)
OutlinedTextField(
value = name,
onValueChange = onNameChange,
label = { Text("Name") }
)
}
}
通过从 HelloContent
中提升出状态,更容易推断该可组合项、在不同的情况下重复使用它,以及进行测试。HelloContent
与状态的存储方式解耦。解耦意味着,如果您修改或替换 HelloScreen
,不必更改 HelloContent
的实现方式。

状态下降、事件上升的这种模式称为“单向数据流”。在这种情况下,状态会从 HelloScreen
下降为 HelloContent
,事件会从 HelloContent
上升为 HelloScreen
。通过遵循单向数据流,您可以将在界面中显示状态的可组合项与应用中存储和更改状态的部分解耦。
如需了解详情,请参阅提升状态的场景页面。
在 Compose 中恢复状态
在重新创建 activity 或进程后,您可以使用 rememberSaveable
恢复界面状态。rememberSaveable
可以在重组后保持状态。此外,rememberSaveable
也可以在重新创建 activity 和进程后保持状态。
存储状态的方式
添加到 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 文档中的状态提升,或者架构指南中更笼统的状态容器和界面状态页面。
在键发生变化时重新触发 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, R.drawable.myDrawable).asAndroidBitmap(),
Shader.TileMode.REPEAT,
Shader.TileMode.REPEAT
)
)
}
remember
会存储该值,直到退出组合。不过,有一种方法可以让缓存值失效。由于 remember
API 也接受 key
或 keys
参数,因此,如果其中有任何键发生变化,那么下次函数重组时,remember
就会让缓存失效并再次对 lambda 块进行计算。这种机制可让您控制组合中对象的生命周期。在输入发生变化之前(而不是在记住的值退出组合之前),计算会一直有效。
以下示例展示了此机制的运作方式。
下面这个代码段会创建一个 ShaderBrush
并将其用作 Box
可组合项的背景绘制。remember
则会存储 ShaderBrush
实例,因为其重建成本高昂(如前所述)。此外,remember
也会接受 avatarRes
作为 key1
参数,即选定的背景图片。如果 avatarRes
发生变化,笔刷会根据新图片进行重组,然后重新应用于 Box
。当用户从选择器中选择另一张图片作为背景时,可能就会发生这种情况。
@Composable
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
,后者可充作 remember
的 key
参数。如果此参数发生变化,应用就需要使用最新的值来重新创建普通状态容器类。例如,当用户旋转设备时,就可能会发生这种情况。
@Composable
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(inputs = typedQuery, stateSaver = TextFieldValue.Saver) {
mutableStateOf(
TextFieldValue(text = typedQuery, selection = TextRange(typedQuery.length))
)
}
了解更多内容
如需详细了解状态和 Jetpack Compose,请参阅下面列出的其他资源。