1. 开始之前
此 Codelab 解释了与在 Jetpack Compose 中使用状态相关的核心概念。它向您展示了应用程序的状态如何决定在 UI 中显示的内容,Compose 如何通过使用不同的 API 在状态更改时更新 UI,如何优化可组合函数的结构以及如何在 Compose 世界中使用 ViewModel。
先决条件
- 了解 Kotlin 语法。
- 了解 Compose 的基本知识(您可以从Jetpack Compose 教程开始)。
- 了解架构组件的
ViewModel
基本知识。
您将学到什么
- 如何在 Jetpack Compose UI 中思考状态和事件。
- Compose 如何使用状态来确定在屏幕上显示哪些元素。
- 什么是状态提升。
- 有状态和无状态的可组合函数是如何工作的。
- Compose 如何使用
State<T>
API 自动跟踪状态。 - 可组合函数中的内存和内部状态是如何工作的:使用
remember
和rememberSaveable
API。 - 如何使用列表和状态:使用
mutableStateListOf
和toMutableStateList
API。 - 如何在 Compose 中使用
ViewModel
。
您需要什么
推荐/可选
- 阅读Compose 思维模式。
- 在学习本 Codelab 之前,先学习Jetpack Compose 基础知识 Codelab。我们将在本 Codelab 中全面回顾状态。
您将构建什么
您将实现一个简单的健康应用程序
该应用程序有两个主要功能
- 一个水计数器,用于跟踪您的饮水量。
- 一个全天待办事项列表。
在您学习本 Codelab 的过程中,如需更多支持,请查看以下代码演示
2. 设置
启动一个新的 Compose 项目
- 要启动一个新的 Compose 项目,请打开 Android Studio。
- 如果您位于“欢迎使用 Android Studio”窗口中,请点击“启动新的 Android Studio 项目”。如果您已打开 Android Studio 项目,请从菜单栏中选择“文件 > 新建 > 新建项目”。
- 对于新项目,从可用模板中选择“空活动”。
- 点击“下一步”并配置您的项目,将其命名为“BasicStateCodelab”。
确保您选择的minimumSdkVersion至少为 API 级别 21,这是 Compose 支持的最低 API 级别。
当您选择“空 Compose 活动”模板时,Android Studio 会在您的项目中为您设置以下内容
- 一个
MainActivity
类,其中配置了一个可组合函数,该函数在屏幕上显示一些文本。 - 一个
AndroidManifest.xml
文件,用于定义应用程序的权限、组件和自定义资源。 - 一个
build.gradle.kts
文件和一个app/build.gradle.kts
文件,其中包含 Compose 所需的选项和依赖项。
Codelab 的解决方案
您可以从 GitHub 获取BasicStateCodelab
的解决方案代码
$ git clone https://github.com/android/codelab-android-compose
或者,您可以将存储库下载为 Zip 文件。
您将在BasicStateCodelab
项目中找到解决方案代码。我们建议您按照自己的节奏逐步学习 Codelab,并在需要帮助时查看解决方案。在 Codelab 中,您会看到需要添加到项目中的代码片段。
3. Compose 中的状态
应用程序的“状态”是任何可能随时间变化的值。这是一个非常广泛的定义,涵盖了从Room 数据库到类中的变量的所有内容。
所有 Android 应用程序都将状态显示给用户。Android 应用程序中状态的一些示例包括
- 聊天应用程序中接收到的最新消息。
- 用户的个人资料照片。
- 项目列表中的滚动位置。
让我们开始编写您的健康应用程序。
为简单起见,在 Codelab 中
- 您可以将所有 Kotlin 文件添加到
app
模块的根com.codelabs.basicstatecodelab
包中。但是,在生产应用程序中,文件应在子包中进行逻辑结构化。 - 您将在代码片段中内联硬编码所有字符串。在实际应用程序中,应将它们作为字符串资源添加到
strings.xml
文件中,并使用 Compose 的stringResource
API 进行引用。
您需要构建的第一个功能是水计数器,用于计算您在一天中饮用的水杯数。
创建一个名为WaterCounter
的可组合函数,其中包含一个Text
可组合项,用于显示水杯数。水杯数应存储在一个名为count
的值中,您可以暂时对其进行硬编码。
创建一个新的文件WaterCounter.kt
,其中包含WaterCounter
可组合函数,如下所示
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
@Composable
fun WaterCounter(modifier: Modifier = Modifier) {
val count = 0
Text(
text = "You've had $count glasses.",
modifier = modifier.padding(16.dp)
)
}
让我们创建一个表示整个屏幕的可组合函数,该函数将有两个部分,水计数器和健康待办事项列表。现在,我们只添加计数器。
- 创建一个文件
WellnessScreen.kt
,它表示主屏幕,并调用我们的WaterCounter
函数
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
@Composable
fun WellnessScreen(modifier: Modifier = Modifier) {
WaterCounter(modifier)
}
- 打开
MainActivity.kt
。删除Greeting
和DefaultPreview
可组合项。在 Activity 的setContent
块内调用新创建的WellnessScreen
可组合项,如下所示
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
BasicStateCodelabTheme {
// A surface container using the 'background' color from the theme
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.background
) {
WellnessScreen()
}
}
}
}
}
- 如果您现在运行应用程序,您将看到我们的基本水计数器屏幕,其中显示了硬编码的水杯数。
可组合函数WaterCounter
的状态是变量count
。但是,静态状态并不是很有用,因为它无法修改。为了解决这个问题,您将添加一个Button
来增加计数并跟踪您全天饮用的水杯数。
任何导致状态修改的操作都称为“事件”,我们将在下一节中详细了解这一点。
4. Compose 中的事件
我们之前将状态描述为任何随时间变化的值,例如,聊天应用程序中接收到的最后一条消息。但是,是什么导致状态更新呢?在 Android 应用程序中,状态会响应事件而更新。
事件是由应用程序外部或内部生成的输入,例如
- 用户通过例如按下按钮与 UI 交互。
- 其他因素,例如传感器发送新值或网络响应。
虽然应用程序的状态提供了 UI 中显示内容的描述,但事件是状态更改的机制,从而导致 UI 发生变化。
事件通知程序的一部分发生了某些事情。在所有 Android 应用程序中,都存在一个核心 UI 更新循环,其工作原理如下
- 事件 - 事件由用户或程序的其他部分生成。
- 更新状态 - 事件处理程序更改 UI 使用的状态。
- 显示状态 - UI 更新以显示新状态。
在 Compose 中管理状态就是了解状态和事件如何相互交互。
现在,添加按钮,以便用户可以通过添加更多水杯来修改状态。
前往 WaterCounter
可组合函数,在我们的标签 Text
下添加 Button
。一个 Column
将帮助你垂直对齐 Text
和 Button
可组合项。你可以将外部填充移动到 Column
可组合项,并在 Button
的顶部添加一些额外的填充,以便将其与 Text 分开。
Button
可组合函数接收一个 onClick
lambda 函数 - 这是按钮被点击时发生的事件。稍后你将看到更多 lambda 函数的示例。
将 count
更改为 var
而不是 val
,使其可变。
import androidx.compose.material3.Button
import androidx.compose.foundation.layout.Column
@Composable
fun WaterCounter(modifier: Modifier = Modifier) {
Column(modifier = modifier.padding(16.dp)) {
var count = 0
Text("You've had $count glasses.")
Button(onClick = { count++ }, Modifier.padding(top = 8.dp)) {
Text("Add one")
}
}
}
当你运行应用程序并点击按钮时,请注意没有任何事情发生。设置 count
变量的不同值不会使 Compose 检测到它是一个状态更改,因此没有任何事情发生。这是因为当状态发生变化时,你没有告诉 Compose 它应该重新绘制屏幕(即“重新组合”可组合函数)。你将在下一步解决此问题。
5. 可组合函数中的内存
Compose 应用程序通过调用可组合函数将数据转换为 UI。我们将组合称为 Compose 在执行可组合项时构建的 UI 描述。如果发生状态更改,Compose 将使用新状态重新执行受影响的可组合函数,从而创建更新的 UI——这称为重新组合。Compose 还查看单个可组合项需要哪些数据,以便它仅重新组合数据已更改的组件,并跳过不受影响的组件。
为了能够做到这一点,Compose 需要知道要跟踪哪些状态,以便在收到更新时可以安排重新组合。
Compose 拥有一个特殊的到位状态跟踪系统,该系统为读取特定状态的任何可组合项安排重新组合。这使 Compose 能够细化并仅重新组合需要更改的可组合函数,而不是整个 UI。这是通过不仅跟踪“写入”(即状态更改),还跟踪对状态的“读取”来实现的。
使用 Compose 的 State
和 MutableState
类型使 Compose 可以观察状态。
Compose 会跟踪读取 State value
属性的每个可组合项,并在其 value
发生更改时触发重新组合。你可以使用 mutableStateOf
函数创建一个可观察的 MutableState
。它接收一个作为参数的初始值,该值包装在 State
对象中,然后使其 value
可观察。
更新 WaterCounter
可组合项,以便 count
使用 mutableStateOf
API,初始值为 0
。由于 mutableStateOf
返回 MutableState
类型,因此你可以更新其 value
以更新状态,Compose 将触发对读取其 value
的函数的重新组合。
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf
@Composable
fun WaterCounter(modifier: Modifier = Modifier) {
Column(modifier = modifier.padding(16.dp)) {
// Changes to count are now tracked by Compose
val count: MutableState<Int> = mutableStateOf(0)
Text("You've had ${count.value} glasses.")
Button(onClick = { count.value++ }, Modifier.padding(top = 8.dp)) {
Text("Add one")
}
}
}
如前所述,对 count
的任何更改都会自动安排读取 count
的 value
的任何可组合函数的重新组合。在这种情况下,每当点击按钮时,WaterCounter
都会重新组合。
如果你现在运行应用程序,你会再次注意到还没有任何事情发生!
安排重新组合工作正常。但是,当发生重新组合时,变量 count
会重新初始化回 0,因此我们需要一种方法来在重新组合中保留此值。
为此,我们可以使用 remember
可组合内联函数。remember
计算出的值在初始组合期间存储在组合中,并且存储的值在重新组合中保持不变。
通常,remember
和 mutableStateOf
在可组合函数中一起使用。
如 Compose State 文档 中所示,有几种等效的方法可以编写此代码。
修改 WaterCounter
,使用 remember
内联可组合函数包围对 mutableStateOf
的调用。
import androidx.compose.runtime.remember
@Composable
fun WaterCounter(modifier: Modifier = Modifier) {
Column(modifier = modifier.padding(16.dp)) {
val count: MutableState<Int> = remember { mutableStateOf(0) }
Text("You've had ${count.value} glasses.")
Button(onClick = { count.value++ }, Modifier.padding(top = 8.dp)) {
Text("Add one")
}
}
}
或者,我们可以通过使用 Kotlin 的 委托属性 来简化 count
的用法。
你可以使用by 关键字将 count
定义为 var。添加委托的 getter 和 setter 导入允许我们间接读取和修改 count
,而无需每次都显式引用 MutableState
的 value
属性。
现在 WaterCounter
看起来像这样
import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue
@Composable
fun WaterCounter(modifier: Modifier = Modifier) {
Column(modifier = modifier.padding(16.dp)) {
var count by remember { mutableStateOf(0) }
Text("You've had $count glasses.")
Button(onClick = { count++ }, Modifier.padding(top = 8.dp)) {
Text("Add one")
}
}
}
你应该选择在编写可组合项时生成最易于阅读代码的语法。
现在让我们检查一下到目前为止我们做了什么
- 定义了一个我们随时间记住的变量,称为
count
。 - 创建了一个文本显示,我们在这里告诉用户我们记住的数字。
- 添加了一个按钮,每当点击它时都会递增我们记住的数字。
这种安排形成了一个与用户的数据流反馈循环。
- UI 向用户呈现状态(当前计数以文本显示)。
- 用户生成的事件与现有状态结合以生成新状态(点击按钮将当前计数加 1)。
你的计数器已准备就绪并可以工作!
6. 状态驱动的 UI
Compose 是一个声明式 UI 框架。当状态发生变化时,我们不是删除 UI 组件或更改其可见性,而是描述 UI 在特定状态条件下是什么。由于调用了重新组合并更新了 UI,可组合项最终可能会进入或离开组合。
这种方法避免了像使用 View 系统那样手动更新视图的复杂性。它也减少了错误的可能性,因为你不会忘记根据新状态更新视图,因为它是自动发生的。
如果在初始组合或重新组合期间调用可组合函数,我们说它在组合中存在。未调用的可组合函数(例如,因为该函数在if 语句中被调用并且条件不满足)在组合中不存在。
你可以在文档中了解有关可组合项的 生命周期 的更多信息。
组合的输出是一个描述 UI 的树状结构。
你可以使用 Android Studio 的布局检查器工具 检查 Compose 生成的应用程序布局,这正是你接下来要做的。
为了演示这一点,修改你的代码以根据状态显示 UI。打开 WaterCounter
,如果 count
大于 0,则显示 Text
。
@Composable
fun WaterCounter(modifier: Modifier = Modifier) {
Column(modifier = modifier.padding(16.dp)) {
var count by remember { mutableStateOf(0) }
if (count > 0) {
// This text is present if the button has been clicked
// at least once; absent otherwise
Text("You've had $count glasses.")
}
Button(onClick = { count++ }, Modifier.padding(top = 8.dp)) {
Text("Add one")
}
}
}
运行应用,然后通过导航到**工具 > 布局检查器**来打开 Android Studio 的布局检查器工具。
您将看到一个分屏:左侧是组件树,右侧是应用的预览。
点击屏幕左侧的根元素BasicStateCodelabTheme
来导航树。点击“展开全部”按钮展开整个组件树。
点击右侧屏幕上的某个元素会导航到该元素在树中的对应位置。
如果您按下应用上的“添加一个”按钮
- 计数增加到 1,状态发生变化。
- 调用重新组合。
- 屏幕使用新元素重新组合。
当您使用 Android Studio 的布局检查器工具检查组件树时,现在您还可以看到Text
可组合项
状态决定了在给定时刻 UI 中存在哪些元素。
UI 的不同部分可以依赖于相同的状态。修改Button
,使其在count
为 10 之前处于启用状态,然后禁用(并完成您当天的目标)。使用Button
的enabled
参数来执行此操作。
@Composable
fun WaterCounter(modifier: Modifier = Modifier) {
...
Button(onClick = { count++ }, Modifier.padding(top = 8.dp), enabled = count < 10) {
...
}
现在运行应用。对状态count
的更改决定是否显示Text
,以及Button
是否启用或禁用。
7. 记住在组合中
remember
将对象存储在组合中,如果在重新组合期间未再次调用remember
所在的源位置,则会忘记该对象。
为了可视化此行为,您将在应用中实现以下功能:当用户至少喝了一杯水时,显示用户需要执行的健康任务,他们也可以关闭该任务。因为可组合项应该小而可重用,所以创建一个名为WellnessTaskItem
的新可组合项,该可组合项根据作为参数接收的字符串显示健康任务,以及一个“关闭”图标按钮。
创建一个新文件WellnessTaskItem.kt
,并添加以下代码。您将在后面的 codelab 中使用此可组合函数。
import androidx.compose.foundation.layout.Row
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.Text
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Close
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.compose.foundation.layout.padding
@Composable
fun WellnessTaskItem(
taskName: String,
onClose: () -> Unit,
modifier: Modifier = Modifier
) {
Row(
modifier = modifier, verticalAlignment = Alignment.CenterVertically
) {
Text(
modifier = Modifier.weight(1f).padding(start = 16.dp),
text = taskName
)
IconButton(onClick = onClose) {
Icon(Icons.Filled.Close, contentDescription = "Close")
}
}
}
WellnessTaskItem
函数接收任务描述和一个onClose
lambda 函数(就像内置的Button
可组合项接收onClick
一样)。
WellnessTaskItem
看起来像这样
为了使用更多功能改进我们的应用,请更新WaterCounter
以在count
> 0 时显示WellnessTaskItem
。
当count
大于 0 时,定义一个变量showTask
来确定是否显示WellnessTaskItem
,并将其初始化为 true。
添加一个新的 if 语句,如果showTask
为 true,则显示WellnessTaskItem
。使用您在前面部分学到的 API 来确保showTask
值在重新组合中得以保留。
@Composable
fun WaterCounter() {
Column(modifier = Modifier.padding(16.dp)) {
var count by remember { mutableStateOf(0) }
if (count > 0) {
var showTask by remember { mutableStateOf(true) }
if (showTask) {
WellnessTaskItem(
onClose = { },
taskName = "Have you taken your 15 minute walk today?"
)
}
Text("You've had $count glasses.")
}
Button(onClick = { count++ }, enabled = count < 10) {
Text("Add one")
}
}
}
使用WellnessTaskItem
的onClose
lambda 函数,以便当按下 X 按钮时,变量showTask
更改为false
,并且不再显示任务。
...
WellnessTaskItem(
onClose = { showTask = false },
taskName = "Have you taken your 15 minute walk today?"
)
...
接下来,添加一个新的Button
,其文本为“清除水量计数”,并将其放在“添加一个”Button
旁边。Row
可以帮助对齐这两个按钮。您还可以向Row
添加一些填充。当按下“清除水量计数”按钮时,变量count
重置回 0。
您的WaterCounter
可组合函数应如下所示
import androidx.compose.foundation.layout.Row
@Composable
fun WaterCounter(modifier: Modifier = Modifier) {
Column(modifier = modifier.padding(16.dp)) {
var count by remember { mutableStateOf(0) }
if (count > 0) {
var showTask by remember { mutableStateOf(true) }
if (showTask) {
WellnessTaskItem(
onClose = { showTask = false },
taskName = "Have you taken your 15 minute walk today?"
)
}
Text("You've had $count glasses.")
}
Row(Modifier.padding(top = 8.dp)) {
Button(onClick = { count++ }, enabled = count < 10) {
Text("Add one")
}
Button(
onClick = { count = 0 },
Modifier.padding(start = 8.dp)) {
Text("Clear water count")
}
}
}
}
当您运行应用时,屏幕会显示初始状态
在右侧,我们有一个简化的组件树版本,它将帮助您分析状态更改时发生的情况。count
和showTask
是记住的值。
现在,您可以在应用中按照以下步骤操作
- 按下“添加一个”按钮。这会递增
count
(这会导致重新组合),并且WellnessTaskItem
和计数器Text
都会开始显示。
- 按下
WellnessTaskItem
组件的 X(这会导致另一次重新组合)。showTask
现在为 false,这意味着WellnessTaskItem
不再显示。
- 按下“添加一个”按钮(另一次重新组合)。
showTask
会记住您已在下次重新组合中关闭了WellnessTaskItem
,如果您继续添加水杯。
- 按下“清除水量计数”按钮以将
count
重置为 0 并导致重新组合。Text
显示count
,以及与WellnessTaskItem
相关的所有代码,都不会被调用并离开组合。
showTask
被遗忘,因为调用 remembershowTask
的代码位置未被调用。您回到了第一步。
- 按下“添加一个”按钮,使
count
大于 0(重新组合)。
WellnessTaskItem
可组合项再次显示,因为当它离开上面的组合时,showTask
的先前值已被遗忘。
如果我们需要showTask
在count
回到 0 后仍然存在,时间比remember
允许的时间更长(也就是说,即使在重新组合期间未调用remember
所在的代码位置)会怎么样?我们将在接下来的部分中探讨如何解决这些场景以及更多示例。
现在您已经了解了当 UI 和状态离开组合时如何重置,请清除您的代码并返回到本节开头处的WaterCounter
@Composable
fun WaterCounter(modifier: Modifier = Modifier) {
Column(modifier = modifier.padding(16.dp)) {
var count by remember { mutableStateOf(0) }
if (count > 0) {
Text("You've had $count glasses.")
}
Button(onClick = { count++ }, Modifier.padding(top = 8.dp), enabled = count < 10) {
Text("Add one")
}
}
}
8. 在 Compose 中恢复状态
运行应用,向计数器中添加一些水杯,然后旋转您的设备。确保已开启设备的自动旋转设置。
因为活动在配置更改(在本例中为方向)后会重新创建,所以保存的状态会被遗忘:计数器会消失并返回到 0。
如果您更改语言、在深色和浅色模式之间切换或执行任何其他导致 Android 重新创建正在运行的活动的配置更改,也会发生同样的情况。
虽然remember
可以帮助您在重新组合之间保留状态,但它**不会在配置更改之间保留**。为此,您必须使用rememberSaveable
而不是remember
。
rememberSaveable
会自动保存任何可以保存在Bundle
中的值。对于其他值,您可以传入自定义保存器对象。有关在 Compose 中恢复状态 的更多信息,请查看文档。
在WaterCounter
中,将remember
替换为rememberSaveable
import androidx.compose.runtime.saveable.rememberSaveable
@Composable
fun WaterCounter(modifier: Modifier = Modifier) {
...
var count by rememberSaveable { mutableStateOf(0) }
...
}
现在运行应用并尝试一些配置更改。您应该会看到计数器已正确保存。
活动重新创建只是rememberSaveable
的用例之一。我们将在以后处理列表时探讨另一个用例。
根据您的应用的状态和 UX 需求,考虑是否使用remember
或rememberSaveable
。
9. 状态提升
使用remember
存储对象的可组合项包含内部状态,这使得可组合项成为有状态的。这在调用方不需要控制状态并且可以使用它而无需自行管理状态的情况下很有用。但是,具有内部状态的可组合项往往可重用性较差,并且难以测试。
不持有任何状态的可组合项称为无状态可组合项。创建无状态可组合项的一种简单方法是使用状态提升。
Compose 中的状态提升是一种将状态移动到可组合项的调用方的模式,以使可组合项成为无状态的。Jetpack Compose 中状态提升的通用模式是用两个参数替换状态变量
- value: T - 要显示的当前值
- onValueChange: (T) -> Unit - 请求使用新值 T 更改值的事件
其中此值表示任何可以修改的状态。
以这种方式提升的状态具有一些重要的属性
- 单一事实来源:通过移动状态而不是复制状态,我们确保只有一个事实来源。这有助于避免错误。
- 可共享:提升的状态可以与多个可组合项共享。
- 可拦截:无状态可组合项的调用方可以在更改状态之前决定忽略或修改事件。
- 解耦:无状态可组合函数的状态可以存储在任何位置。例如,在 ViewModel 中。
尝试为WaterCounter
实现此功能,以便它可以从上述所有功能中获益。
有状态与无状态
当所有状态都可以从可组合函数中提取时,生成的组合函数称为无状态的。
通过将其拆分为两个部分:有状态和无状态计数器来重构WaterCounter
可组合项。
StatelessCounter
的作用是显示count
并在您递增count
时调用函数。为此,请遵循上面描述的模式并传递状态count
(作为可组合函数的参数)和一个 lambda(onIncrement
),当需要递增状态时会调用该 lambda。StatelessCounter
看起来像这样
@Composable
fun StatelessCounter(count: Int, onIncrement: () -> Unit, modifier: Modifier = Modifier) {
Column(modifier = modifier.padding(16.dp)) {
if (count > 0) {
Text("You've had $count glasses.")
}
Button(onClick = onIncrement, Modifier.padding(top = 8.dp), enabled = count < 10) {
Text("Add one")
}
}
}
StatefulCounter
拥有状态。这意味着它持有 count
状态并在调用 StatelessCounter
函数时修改它。
@Composable
fun StatefulCounter(modifier: Modifier = Modifier) {
var count by rememberSaveable { mutableStateOf(0) }
StatelessCounter(count, { count++ }, modifier)
}
干得好!你将 count
从 StatelessCounter
提到了 StatefulCounter
中。
你可以将它插入你的应用,并使用 StatefulCounter
更新 WellnessScreen
。
@Composable
fun WellnessScreen(modifier: Modifier = Modifier) {
StatefulCounter(modifier)
}
如前所述,状态提升有一些好处。我们将探索此代码的不同变体来解释其中的一些,你不需要在你的应用中复制以下代码段。
- 你的无状态可组合函数现在可以重用了。例如,以下示例。
为了计算水杯和果汁杯的数量,你需要记住 waterCount
和 juiceCount
,但使用相同的 StatelessCounter
可组合函数来显示两个不同的独立状态。
@Composable
fun StatefulCounter() {
var waterCount by remember { mutableStateOf(0) }
var juiceCount by remember { mutableStateOf(0) }
StatelessCounter(waterCount, { waterCount++ })
StatelessCounter(juiceCount, { juiceCount++ })
}
如果修改了 juiceCount
,则 StatefulCounter
将重新组合。在重新组合期间,Compose 会识别哪些函数读取 juiceCount
并仅触发这些函数的重新组合。
当用户点击递增 juiceCount
时,StatefulCounter
会重新组合,读取 juiceCount
的 StatelessCounter
也会重新组合。但读取 waterCount
的 StatelessCounter
不会重新组合。
- 你的有状态可组合函数可以向多个可组合函数提供相同的状态.
@Composable
fun StatefulCounter() {
var count by remember { mutableStateOf(0) }
StatelessCounter(count, { count++ })
AnotherStatelessMethod(count, { count *= 2 })
}
在这种情况下,如果计数由 StatelessCounter
或 AnotherStatelessMethod
其中一个更新,则所有内容都将重新组合,这是预期的行为。
因为提升的状态可以共享,所以请确保只传递可组合函数需要的状态,以避免不必要的重新组合,并提高可重用性。
要了解更多关于状态和状态提升的信息,请查看 Compose 状态文档。
10. 使用列表
接下来,添加应用的第二个功能,健康任务列表。你可以对列表中的项目执行两个操作
- 选中列表项以将任务标记为已完成。
- 删除你不感兴趣完成的任务。
设置
- 首先,修改列表项。你可以重用“在组合中记住”部分中的
WellnessTaskItem
,并更新它以包含Checkbox
。确保你提升了checked
状态和onCheckedChange
回调以使函数无状态。
本节的 WellnessTaskItem
可组合函数应该如下所示
import androidx.compose.material3.Checkbox
@Composable
fun WellnessTaskItem(
taskName: String,
checked: Boolean,
onCheckedChange: (Boolean) -> Unit,
onClose: () -> Unit,
modifier: Modifier = Modifier
) {
Row(
modifier = modifier, verticalAlignment = Alignment.CenterVertically
) {
Text(
modifier = Modifier
.weight(1f)
.padding(start = 16.dp),
text = taskName
)
Checkbox(
checked = checked,
onCheckedChange = onCheckedChange
)
IconButton(onClick = onClose) {
Icon(Icons.Filled.Close, contentDescription = "Close")
}
}
}
- 在同一文件中,添加一个有状态的
WellnessTaskItem
可组合函数,该函数定义一个状态变量checkedState
并将其传递给同名的无状态方法。现在不用担心onClose
,你可以传递一个空 lambda 函数。
import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
@Composable
fun WellnessTaskItem(taskName: String, modifier: Modifier = Modifier) {
var checkedState by remember { mutableStateOf(false) }
WellnessTaskItem(
taskName = taskName,
checked = checkedState,
onCheckedChange = { newValue -> checkedState = newValue },
onClose = {}, // we will implement this later!
modifier = modifier,
)
}
- 创建一个名为
WellnessTask.kt
的文件来模拟一个包含 ID 和标签的任务。将其定义为一个 数据类。
data class WellnessTask(val id: Int, val label: String)
- 对于任务列表本身,创建一个名为
WellnessTasksList.kt
的新文件,并添加一个生成一些伪数据的方法
fun getWellnessTasks() = List(30) { i -> WellnessTask(i, "Task # $i") }
请注意,在真实的应用中,你从你的 数据层 获取数据。
- 在
WellnessTasksList.kt
中,添加一个创建列表的可组合函数。定义一个LazyColumn
和你创建的列表方法中的项。如果需要帮助,请查看 列表文档。
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.runtime.remember
@Composable
fun WellnessTasksList(
modifier: Modifier = Modifier,
list: List<WellnessTask> = remember { getWellnessTasks() }
) {
LazyColumn(
modifier = modifier
) {
items(list) { task ->
WellnessTaskItem(taskName = task.label)
}
}
}
- 将列表添加到
WellnessScreen
。使用Column
来帮助将列表与你已有的计数器垂直对齐。
import androidx.compose.foundation.layout.Column
@Composable
fun WellnessScreen(modifier: Modifier = Modifier) {
Column(modifier = modifier) {
StatefulCounter()
WellnessTasksList()
}
}
- 运行应用并试一试!你现在应该能够选中任务,但不能删除它们。你将在后面的部分实现这一点。
在 LazyList 中恢复项目状态
现在仔细看看 WellnessTaskItem
可组合函数中的一些内容。
checkedState
独立属于每个 WellnessTaskItem
可组合函数,就像一个私有变量。当 checkedState
发生变化时,只有 WellnessTaskItem
的该实例会重新组合,而不是 LazyColumn
中的所有 WellnessTaskItem
实例。
按照以下步骤尝试:
- 选中此列表顶部的任何元素(例如元素 1 和 2)。
- 滚动到列表底部,使它们离开屏幕。
- 滚动回顶部到之前选中的项目。
- 注意它们未被选中。
正如你在前面部分看到的,存在一个问题,即当项目离开 Composition 时,记住的状态会被遗忘。对于 LazyColumn
上的项目,当你滚动经过它们并且它们不再可见时,这些项目会完全离开 Composition。
如何解决这个问题?再次使用 rememberSaveable
。你的状态将使用保存的实例状态机制在活动或进程重新创建中存活下来。由于 rememberSaveable
与 LazyList
的协同工作方式,你的项目也能够在离开 Composition 后存活下来。
只需在你的有状态 WellnessTaskItem
中用 rememberSaveable
替换 remember
,就完成了。
import androidx.compose.runtime.saveable.rememberSaveable
var checkedState by rememberSaveable { mutableStateOf(false) }
Compose 中的常见模式
注意 LazyColumn
的实现
@Composable
fun LazyColumn(
...
state: LazyListState = rememberLazyListState(),
...
可组合函数 rememberLazyListState
使用 rememberSaveable
为列表创建初始状态。当 Activity 重新创建时,滚动状态会得到保留,而无需你编写任何代码。
许多应用需要对滚动位置、项目布局更改以及与列表状态相关的其他事件做出反应并监听。像 LazyColumn
或 LazyRow
这样的惰性组件通过提升 LazyListState
来支持此用例。你可以在 列表状态文档 中了解更多关于此模式的信息。
使用由公共 rememberX
函数提供的默认值的状态参数是内置可组合函数中的常见模式。另一个示例可以在 BottomSheetScaffold
中找到,它使用 rememberBottomSheetScaffoldState
提升状态。
11. 可观察的 MutableList
接下来,要添加从列表中删除任务的行为,第一步是将你的列表变成可变列表。
使用可变对象来实现这一点,例如 ArrayList<T>
或 mutableListOf
,将不起作用。这些类型不会通知 Compose 列表中的项目已更改并安排 UI 的重新组合。你需要一个不同的 API。
你需要创建一个 MutableList
实例,该实例可以被 Compose 观察。此结构允许 Compose 跟踪更改,以便在向列表中添加或删除项目时重新组合 UI。
首先定义我们的可观察 MutableList
。扩展函数 toMutableStateList()
是从初始可变或不可变 Collection
(如 List
)创建可观察 MutableList
的方法。
或者,你也可以使用工厂方法 mutableStateListOf
创建可观察 MutableList
,然后添加初始状态的元素。
- 打开
WellnessScreen.kt
文件。将getWellnessTasks
方法移动到此文件,以便能够使用它。通过首先调用getWellnessTasks()
,然后使用之前学习的扩展函数toMutableStateList
来创建列表。
import androidx.compose.runtime.remember
import androidx.compose.runtime.toMutableStateList
@Composable
fun WellnessScreen(modifier: Modifier = Modifier) {
Column(modifier = modifier) {
StatefulCounter()
val list = remember { getWellnessTasks().toMutableStateList() }
WellnessTasksList(list = list, onCloseTask = { task -> list.remove(task) })
}
}
private fun getWellnessTasks() = List(30) { i -> WellnessTask(i, "Task # $i") }
- 修改
WellnessTasksList
可组合函数,移除列表的默认值,因为列表已提升到屏幕级别。添加一个新的 lambda 函数参数onCloseTask
(接收要删除的WellnessTask
)。将onCloseTask
传递给WellnessTaskItem
。
您还需要进行一项更改。items
方法接收一个 key
参数。默认情况下,每个项目的 state 都会根据其在列表中的位置作为 key。
在可变列表中,当数据集发生变化时,这会导致问题,因为位置发生变化的项目实际上会丢失任何记住的 state。
您可以通过使用每个 WellnessTaskItem
的 id
作为每个项目的 key 来轻松解决此问题。
要了解有关列表中的项目键的更多信息,请查看文档。
WellnessTasksList
将如下所示
@Composable
fun WellnessTasksList(
list: List<WellnessTask>,
onCloseTask: (WellnessTask) -> Unit,
modifier: Modifier = Modifier
) {
LazyColumn(modifier = modifier) {
items(
items = list,
key = { task -> task.id }
) { task ->
WellnessTaskItem(taskName = task.label, onClose = { onCloseTask(task) })
}
}
}
- 修改
WellnessTaskItem
:将onClose
lambda 函数作为参数添加到 stateful 的WellnessTaskItem
中并调用它。
@Composable
fun WellnessTaskItem(
taskName: String, onClose: () -> Unit, modifier: Modifier = Modifier
) {
var checkedState by rememberSaveable { mutableStateOf(false) }
WellnessTaskItem(
taskName = taskName,
checked = checkedState,
onCheckedChange = { newValue -> checkedState = newValue },
onClose = onClose,
modifier = modifier,
)
}
干得好!功能已完成,从列表中删除项目的功能正常工作。
如果点击每一行中的 X,事件会一直传递到拥有 state 的列表,从列表中删除项目并导致 Compose 重新组合屏幕。
如果尝试使用 rememberSaveable()
在 WellnessScreen
中存储列表,则会收到运行时异常
此错误告诉您需要提供一个自定义 Saver。但是,您不应使用rememberSaveable
存储大量数据或需要冗长序列化或反序列化的复杂数据结构。
在使用 Activity 的onSaveInstanceState
时,也适用类似的规则;您可以在保存 UI state 文档中找到更多信息。如果要执行此操作,则需要其他存储机制。您可以在文档中了解有关保留 UI state 的不同选项的更多信息。
接下来,我们将了解 ViewModel 作为应用程序 state 持有者的作用。
12. ViewModel 中的 State
屏幕或 UI state 指示应在屏幕上显示什么(例如,任务列表)。此 state 通常与层次结构的其他层连接,因为它包含应用程序数据。
虽然 UI state 描述了在屏幕上显示什么,但应用程序的逻辑描述了应用程序的行为以及如何对 state 更改做出反应。逻辑有两种类型:UI 行为或 UI 逻辑,以及业务逻辑。
- UI 逻辑涉及如何在屏幕上显示 state 更改(例如,导航逻辑或显示 Snackbar)。
- 业务逻辑是如何处理 state 更改(例如进行付款或存储用户偏好)。此逻辑通常放在业务或数据层中,永远不要放在 UI 层中。
ViewModel 提供 UI state 并访问位于应用程序其他层中的业务逻辑。此外,ViewModel 在配置更改后仍然存在,因此它们的生存期比 Composition 更长。它们可以遵循 Compose 内容宿主(即 Activity、Fragment 或 Navigation 图的目的地,如果您使用的是Compose Navigation)的生命周期。
要了解有关架构和 UI 层的更多信息,请查看UI 层文档。
迁移列表和移除方法
虽然前面的步骤向您展示了如何在可组合函数中直接管理 state,但最佳实践是将 UI 逻辑和业务逻辑与 UI state 分离,并将其迁移到 ViewModel 中。
让我们将 UI state(列表)迁移到您的 ViewModel 中,并开始将业务逻辑提取到其中。
- 创建一个文件
WellnessViewModel.kt
以添加您的 ViewModel 类。
将您的“数据源” getWellnessTasks()
移动到 WellnessViewModel
中。
定义一个内部 _tasks
变量,使用 toMutableStateList
(如前所述),并将 tasks
公开为列表,这样它就不能从 ViewModel 外部修改。
实现一个简单的 remove
函数,该函数委托给列表的内置 remove 函数。
import androidx.compose.runtime.toMutableStateList
import androidx.lifecycle.ViewModel
class WellnessViewModel : ViewModel() {
private val _tasks = getWellnessTasks().toMutableStateList()
val tasks: List<WellnessTask>
get() = _tasks
fun remove(item: WellnessTask) {
_tasks.remove(item)
}
}
private fun getWellnessTasks() = List(30) { i -> WellnessTask(i, "Task # $i") }
- 我们可以通过调用
viewModel()
函数从任何可组合项访问此 ViewModel。
要使用此函数,请打开 app/build.gradle.kts
文件,添加以下库,并在 Android Studio 中同步新的依赖项
implementation("androidx.lifecycle:lifecycle-viewmodel-compose:{latest_version}")
在使用 Android Studio Giraffe 时,请使用版本2.6.2
。否则,请在此处查看库的最新版本此处。
- 打开
WellnessScreen
。通过调用viewModel()
实例化wellnessViewModel
ViewModel,作为 Screen 可组合项的参数,以便在测试此可组合项时可以替换它,并在需要时提升它。将任务列表和删除函数提供给WellnessTasksList
的onCloseTask
lambda。
import androidx.lifecycle.viewmodel.compose.viewModel
@Composable
fun WellnessScreen(
modifier: Modifier = Modifier,
wellnessViewModel: WellnessViewModel = viewModel()
) {
Column(modifier = modifier) {
StatefulCounter()
WellnessTasksList(
list = wellnessViewModel.tasks,
onCloseTask = { task -> wellnessViewModel.remove(task) })
}
}
viewModel()
返回现有的 ViewModel
或在给定范围内创建一个新的 ViewModel
。只要范围有效,ViewModel 实例就会被保留。例如,如果可组合项在 Activity 中使用,则 viewModel()
会返回相同的实例,直到 Activity 结束或进程被终止。
就是这样!您已将 ViewModel 与部分 state 和业务逻辑集成到您的屏幕中。由于 state 保存在 Composition 之外并由 ViewModel 存储,因此对列表的更改在配置更改后仍然存在。
ViewModel 不会在任何情况下自动持久化应用程序的 state(例如,对于系统启动的进程终止)。有关持久化应用程序 UI state的详细信息,请查看文档。
迁移选中状态
最后一个重构是将选中状态和逻辑迁移到 ViewModel。这样,代码更简单且更易于测试,所有 state 都由 ViewModel 管理。
- 首先,修改
WellnessTask
模型类,使其能够存储选中状态并将 false 作为默认值。
data class WellnessTask(val id: Int, val label: String, var checked: Boolean = false)
- 在 ViewModel 中,实现一个方法
changeTaskChecked
,该方法接收要修改的任务以及选中状态的新值。
class WellnessViewModel : ViewModel() {
...
fun changeTaskChecked(item: WellnessTask, checked: Boolean) =
_tasks.find { it.id == item.id }?.let { task ->
task.checked = checked
}
}
- 在
WellnessScreen
中,通过调用 ViewModel 的changeTaskChecked
方法为列表的onCheckedTask
提供行为。这些函数现在应如下所示
@Composable
fun WellnessScreen(
modifier: Modifier = Modifier,
wellnessViewModel: WellnessViewModel = viewModel()
) {
Column(modifier = modifier) {
StatefulCounter()
WellnessTasksList(
list = wellnessViewModel.tasks,
onCheckedTask = { task, checked ->
wellnessViewModel.changeTaskChecked(task, checked)
},
onCloseTask = { task ->
wellnessViewModel.remove(task)
}
)
}
}
- 打开
WellnessTasksList
并添加onCheckedTask
lambda 函数参数,以便您可以将其传递给WellnessTaskItem.
。
@Composable
fun WellnessTasksList(
list: List<WellnessTask>,
onCheckedTask: (WellnessTask, Boolean) -> Unit,
onCloseTask: (WellnessTask) -> Unit,
modifier: Modifier = Modifier
) {
LazyColumn(
modifier = modifier
) {
items(
items = list,
key = { task -> task.id }
) { task ->
WellnessTaskItem(
taskName = task.label,
checked = task.checked,
onCheckedChange = { checked -> onCheckedTask(task, checked) },
onClose = { onCloseTask(task) }
)
}
}
}
- 清理
WellnessTaskItem.kt
文件。我们不再需要 stateful 方法,因为 CheckBox 状态将提升到 List 级别。该文件仅包含此可组合函数
@Composable
fun WellnessTaskItem(
taskName: String,
checked: Boolean,
onCheckedChange: (Boolean) -> Unit,
onClose: () -> Unit,
modifier: Modifier = Modifier
) {
Row(
modifier = modifier, verticalAlignment = Alignment.CenterVertically
) {
Text(
modifier = Modifier
.weight(1f)
.padding(start = 16.dp),
text = taskName
)
Checkbox(
checked = checked,
onCheckedChange = onCheckedChange
)
IconButton(onClick = onClose) {
Icon(Icons.Filled.Close, contentDescription = "Close")
}
}
}
- 运行应用程序并尝试选中任何任务。请注意,选中任何任务目前还无法正常工作。
这是因为 Compose 跟踪 MutableList
的是与添加和移除元素相关的更改。这就是删除功能正常工作的原因。但它不知道行项目值(在本例中为 checkedState
)的更改,除非您告诉它也跟踪这些更改。
有两种方法可以解决此问题
- 更改我们的数据类
WellnessTask
,以便checkedState
成为MutableState<Boolean>
而不是Boolean
,这会导致 Compose 跟踪项目更改。 - 复制您要变异的项目,从列表中移除该项目,然后将变异后的项目重新添加到列表中,这会导致 Compose 跟踪该列表更改。
这两种方法都各有优缺点。例如,根据您使用的列表的实现,移除和读取元素可能会很耗费资源。
因此,假设您想避免潜在的昂贵列表操作,并使 checkedState
可观察,因为它更有效且更符合 Compose 的习惯用法。
您的新 WellnessTask
可以如下所示
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf
data class WellnessTask(val id: Int, val label: String, val checked: MutableState<Boolean> = mutableStateOf(false))
如您之前所见,您可以使用委托属性,这会导致在此情况下更简单的变量 checked
用法。
将 WellnessTask
从数据类更改为普通类。使 WellnessTask
在构造函数中接收一个名为 initialChecked
的变量,其默认值为 false
,然后我们可以使用工厂方法 mutableStateOf
初始化 checked
变量,并使用 initialChecked
作为默认值。
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
class WellnessTask(
val id: Int,
val label: String,
initialChecked: Boolean = false
) {
var checked by mutableStateOf(initialChecked)
}
就是这样!此解决方案有效,并且所有更改都将在重新组合和配置更改后保留!
测试
现在业务逻辑已重构到 ViewModel 中,而不是耦合在可组合函数内,因此单元测试变得更加简单。
您可以使用仪器测试来验证 Compose 代码的正确行为以及 UI 状态是否正常工作。考虑学习 Compose 中的测试 代码实验室,了解如何测试您的 Compose UI。
13. 恭喜
干得好!您已成功完成本代码实验室,并学习了在 Jetpack Compose 应用中使用状态的所有基本 API!
您学习了如何思考状态和事件以在 Compose 中提取无状态的可组合项,以及 Compose 如何使用状态更新来驱动 UI 中的变化。
接下来是什么?
查看 Compose 学习路径 上的其他代码实验室。
示例应用
- JetNews 演示了本代码实验室中解释的最佳实践。