应用中的状态是指随时间变化的任何值。这是一个非常宽泛的定义,涵盖了从 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
可帮助您在重组时保留状态,但状态不会在配置更改时保留。为此,您必须使用 rememberSaveable
。rememberSaveable
会自动保存可以存储在 Bundle
中的任何值。对于其他值,您可以传入自定义 Saver 对象。
其他受支持的状态类型
Compose 不要求您使用 MutableState<T>
来持有状态;它支持其他可观察类型。在 Compose 中读取其他可观察类型之前,您必须将其转换为 State<T>
,以便可组合项在状态更改时自动重组。
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.8.7")
}
Groovy
dependencies {
...
implementation "androidx.lifecycle:lifecycle-runtime-compose:2.8.7"
}
-
collectAsState
类似于collectAsStateWithLifecycle
,因为它也从Flow
收集值并将其转换为 ComposeState
。对于平台无关的代码,请使用
collectAsState
,而不是仅适用于 Android 的collectAsStateWithLifecycle
。collectAsState
不需要额外的依赖项,因为它在compose-runtime
中可用。 -
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"
}
-
subscribeAsState()
是扩展函数,可将 RxJava2 的响应流(例如Single
、Observable
、Completable
)转换为 ComposeState
。以下 依赖项 在
build.gradle
文件中是必需的:
Kotlin
dependencies {
...
implementation("androidx.compose.runtime:runtime-rxjava2:1.8.1")
}
Groovy
dependencies {
...
implementation "androidx.compose.runtime:runtime-rxjava2:1.8.1"
}
-
subscribeAsState()
是扩展函数,可将 RxJava3 的响应流(例如Single
、Observable
、Completable
)转换为 ComposeState
。以下 依赖项 在
build.gradle
文件中是必需的:
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
中。
在本示例中,您将 name
和 onValueChange
从 HelloContent
中提取出来,并将它们向上移动到调用 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.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 还接受 key
或 keys
参数。如果其中任何键发生更改,下次函数重组时,remember
会使缓存失效并再次执行计算 lambda 块。此机制让您可以控制组合中对象的生命周期。计算结果在输入发生更改之前一直有效,而不是直到记住的值离开组合为止。
以下示例展示了此机制的工作原理。
在此代码段中,会创建 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
,它用作 remember
的 key
参数。如果此参数发生更改,应用需要使用最新值重新创建普通状态持有者类。例如,如果用户旋转设备,可能会发生这种情况。
@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
视频
博客
为您推荐
- 注意:当 JavaScript 关闭时,会显示链接文本
- 设计您的 Compose 界面架构
- 在 Compose 中保存界面状态
- Compose 中的副作用