六、组合中的记忆功能
remember
会将对象存储在组合中,而如果在重组期间再次调用之前调用remember
的来源未知,则会忘记对象。
为了直观呈现这种行为,我们将在应用中实现以下功能:当用户至少饮用了一杯水时,向用户显示有一项待执行的健康任务,同时用户也可以关闭此任务。由于可组合项应较小并可重复使用,因此请创建一个名为WellnessTaskItem
的新可组合项,该可组合根据以参数形式接收的字符串来显示健康任务,并显示一个Close图标按钮。
创建一个新文件WellnessTaskItem.kt
,并添加以下代码。
import androidx.compose.foundation.layout.Row
import androidx.compose.material.Icon
import androidx.compose.material.IconButton
import androidx.compose.material.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 ++ }, enable = count < 10) {
Text("Add one")
}
}
}
使用WellnessTaskItem
的onClose
lambda函数实现:在按下X按钮时,变量showTask
更改为false
,且不再显示任务。
...
WellnessTaskItem(
onClose = { showTask = false },
taskName = "Have you taken your 15 minute walk today?"
)
...
接下来,添加一个代“Clear water count”文本的新Button
,并将其放置在“Add one” Buttton
旁边。Row
可帮助对其两个按钮。您还可以向Row
添加一些内边距。按下“Clear water count”按钮后,变量count
会重置为0。
WaterCounter
可组合函数应如下所示。
import androix.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'vve 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
是记住的值。
现在,您可以在应用中按以下步骤操作:
- 按下Add one按钮。此操作会递增
count
(这会导致重组),并同时显示WellnessTaskItem
和计数器Text
。
- 按下
WellnessTaskItem
组件的X(这会导致另一项重组)。showTask
现在为false,这意味着不再显示WellnessTaskItem
。
- 按下Add one按钮(另一项重组)。如果您继续增加杯数,
showTask
会记住您在下一次重组时关闭了WellnessTaskItem
。
- 按下Clear water count按钮可将
count
重置为0并导致重组。系统不会调用显示count
的Text
以及与WellnessTaskItem
相关的代码,并且会退出组合。
- 由于系统未调用之前调用
showTask
的代码位置,因此会忘记showTask
。这将返回第一步。
- 按下Add one按钮,使
count
大于0(重组)。
- 系统再次显示
WellnessTaskItem
可组合项,因为在退出上述组合时,之前的showTask
值已被忘记。
如果我们要求showTask
在count
重置为0之后持续保留超过remember
允许的时间(也就是说,即使重组期间未调用之前调用remember
的代码位置),会发生什么?在接下来的部分中,我们将探讨如何修正这些问题以及更多示例。
现在,您已经了解了界面和状态在退出组合后的重置过程,请清除代码并返回到本部分开头的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")
}
}
}
七、在Compose中恢复状态
运行应用,为计数器增加一些饮水杯数,然后旋转设备。请确保已为设备启用自动屏幕旋转设置。
由于系统会在配置更改后(在本例中,即改变屏幕方向)重新创建activity,因此已保存状态会被忘记:计数器会在重置为0后消失。
如果您更改语言、在深色模式与浅色模式之间切换,或者执行任何导致Android重新创建运行中activity的其他配置更改时,也会发生相同的情况。
虽然remember
可帮助您在重组后保持状态,但不会帮助您在配置更改后保持状态。为此,您必须使用rememberSaveable
,而不是remember
。
rememberSaveable
会自动保存可保存在Bundle
中的任何值,对于其他值,您可以将其传入自定义Saver对象。
在WaterCounter
中,将remember
替换为rememberSaveable
:
import androidx.compose.runtime.saveable.rememberSaveable
@Composable
fun WaterCounter(modifier: Modifier = Modifier) {
...
var count by rememberSaveable { mutableStateOf(0) }
...
}
现在运行应用并尝试进行一些配置更改。您应该会看到计数器已正确保存。
重新创建activity只是rememberSaveable
的用例之一。
在重新创建activity或进程后,您可以使用rememberSaveable
恢复界面状态。除了在重组后保持状态之外,rememberSaveable
还会重新创建activity和进程之后保留状态。
请根据应用的状态和用户体验需求来考虑是使用remember
还是rememberSaveable
。
八、提升状态
使用remember
存储对象的可组合项包含内部状态,这会使该可组合项有状态。在调用方不需要控制状态,并且不必自行管理状态便可使用状态的情况,“有状态”会非常有用 。但是,具有内部状态的可组合往往不易重复使用 ,也更难测试。
不保存任何状态的可组合项称为无状态可组合项。如需创建无状态可组合项,一种简单的方法是使用状态提升。
Compose中的状态提升是一种将状态移至可组合项的调用方以使可组合项无状态的模式。Jetpack Compose中的常规状态提升模式是将状态变量替换为两个参数。
- value: T:要显示的当前值
- onValueChange:(T)-> Unit:请求更改值得事件,其中T是建议的新值。
其中,此值表示任何可修改的状态。
状态下降、事件上升的这种模式称为单向数据流(UDF),而状态提升就是我们在Compose中实现 此架构的方式。
以这种方式提升的状态具有一些重要的属性:
- 单一可信来源:通过移动状态,而不是复制状态,我们可确保只有一个可信来源。这有助于避免bug。
- 可共享:可与多个可组合向共享提升的状态。
- 可拦截:无状态可组合项的调用方可以在更改状态之前决定忽略或修改事件。
- 分离:无状态 可组合函数的状态可以存储在任何位置。例如,存储在ViewModel中。
请尝试WaterCounter
实现状态提升,以便从以上所有的方法中受益。
8.1、有状态与无状态
当所有状态都可以从可组合函数中提取出来时,生成的可组合函数称为无状态函数。
无状态可组合项是指不具有任何状态的可组合项,这意味着他不会存储、定义或修改新状态。
有状态可组合项是一种具有可以随事件变化的状态的可组合项。
在实际应用中,让可组合项100%完全无状态可能很难实现,具体取决于可组合项的职责。在设计可组合项时,您应该让可组合项用友尽可能少的状态,并能够在必要时通过在可组合项的API中公开状态来提升状态。
重构WaterCounter
可组合项 ,将其拆分为两部分:有状态和无状态计数器。
StatelessCounter
的作用是显示count
,并在您递增count
时调用函数。为此,请遵循上述模式并传递状态count
(作为可组合函数的参数 )和lambda(onIncrement
)(在需要递增状态时会调用此函数)。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), enable = 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 WellnesScreen(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, { waterCounter++ })
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
更新计数,则系统会按预期重组所有项目。
由于可以共享提升的状态,因此请务必仅传递可组合项所需的状态,以避免不必要的重组并提高可重用性。
要点:设计可组合项的最佳实践仅向它们传递所需要的参数。
九、使用列表
接下来,添加应用的第二项功能,即健康任务列表。您可以对列表中的项执行以下两项操作:
- 勾选列表项,将任务标记为已完成。
- 从任务列表中移除不想完成的任务。
9.1、设置
- 首先,修改列表项。您可以重复使用“组合中的记忆功能”部分中的
WellnessTaskItem
,并将其更新为包含Checkbox
。请务必提升checked
状态和onCheckedChange
回调,使函数变为无状态。
本部分的WellnessTaskItem
可组合项应如下所示:
import androidx.compose.material.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, conntentDescription = "Close")
}
}
}
- 在同一文件中,添加一个有状态
WellnessTaskItem
可组合函数,用于定义状态变量checkedState
并将其传递给同名的无状态方法。暂时不用担心onClose
,您可以传递空的lambda函数。
import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtiime.remember
@Composable
fun WellnessTaskItem(taskName: String, modifier: Modifier = Modifier) {
var checkedState by remember { mutableStateOf(false) }
WekknessTaskItem(
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
的新文件,并添加一个方法用于生成一些虚假数据:
private 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
有助于列表与已有的计数器垂直对齐。注意:如果在Android Studio的编辑区域键入WC,系统会打开一个建议框 。如果您按下Enter并选择第一个选项,系统会显示可供使用的Column模板。
import androidx.compose.foundation.layout.Column
@Composable
fun WellnessScreen(modifier: Modifier = Modifier) {
Column(modifier = modifier) {
StatefulCounter()
WellnessTasksList()
}
}
7.运行应用并试一下效果!现在,您应该能够勾选任务,但不能删除任务。
9.2、在LazyList中恢复项状态
现在,我们来详细了解一下WellnessTaskItem
可组合项中的一些内容。
checkedState
属于每个WellnessTaskItem
可组合项,就像私有变量一样。当checkedState
发生变化时,系统只会重绘WellnessTaskItem
的实例,而不是重组LazyColumn
中的所有WellnessTaskItem
实例。
请按以下步骤尝试应用的功能:
- 勾选此列表项部的所有元素(例如元素1和元素2)
- 滚动到列表底部,使这些元素位于屏幕之外。
- 滚动到顶部,查看之前勾选的列表项。
- 请注意,它们处于未选中状态。
正如您在上一部分中看到的那样,其问题在于,当一个项退出组合时,系统会忘记之前记住的状态。对于LazyColumn
上的项,当年您滚动至项不可见的位置时,这些不可见的项会完全退出组合。
如何解决此问题?同样,使用rememberSaveable
。它采用保存的实例状态机制,可确保存储的值在重新创建activity或进程之后继续保留。得益于rememberSaveable
与LazyList
配合工作的方式,您的项在离开组合后也能继续保留。
只需在有状态WellnessTaskItem
中将remember
替换为rememberSaveable
即可,如下所示:
import androidx.compose.runtime.saveable.rememberSaveable
var checkedState by rememberSaveable { mutableStateOf(false) }
9.3、Compose中的常见模式
请注意LazyColumn
的实现:
@Composable
fun lazyColumn(
...
state: LazyListState = rememberLazyListState(),
...
)
可组合函数rememberLazyListState
使用rememberSaveable
为列表创建初始状态。重新创建activity后,无需任何编码即可保持滚动状态。
许多应用更需要对滚动位置、列表项布局更改以及其他与列表状态相关的事件作出响应,并进行监听。延迟组件(例如LazyColumn
或LazyRow
)可通过提升LazyListState
来支持用例。
状态参数使用由公共rememberX
函数提供的默认值时内置可组合函数中的常见模式。
十、可观察的可变列表
接下来,如需添加从列表中移除任务的行为,第一步是让列表成为可变列表。
使用可变对象(例如ArrayList<T>
或mutableListOf
),对此不起作用。这些类型不会向Compose通知列表中的项已发生更改并安排界面重组。您需要使用其他API。
您需要创建一个可由Compose观察的MutableList
实例。此结构可允许Compose跟踪更改,以便在列表中添加或移除项时重组界面。
首先,定义可观察MutableList
。扩展函数toMutableStateList()
用于根据初始可变或不可变的Collection
(例如List
)来创建可观察的MutableList
。
或者,您也可以使用工厂方法mutableStateListOf
来创建可观察的MutableList
,然后为初始状态添加元素。
mutableStateOf函数会返回一个类型为MutableState<T>的对象。
mutableStateListOf和toMutableStateList函数会返回一个类型为SnapshotStateList<T>的对象。
- 打开
WellnessScreen.kt
文件。将getWellnessTasks
方法移至此文件中以便使用该方法。如需创建列表,请先调用getWellnessTasks()
,然后使用之前介绍的扩展函数toMutableStateList
。
import androidx.compose.runtime.remeber
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")}
警告:您可以改为mutableStateListOf
API来创建列表。但是,如果使用方法不当,则可能会导致意外重组和界面性能欠佳。
如果您仅定义列表,然后在不同的操作中添加任务,则会导致系统在每次重组时都添加重复项。
// Don't do this
val list = remember { mutableStateListOf<WellnessTask>() }
list.addAll(getWellnessTasks())
而是应该在单一操作中创建包含初始值的列表,然后将其传递给remember
函数,如下所示:
// Do this instead. Don't need to copy
val list = remember {
mutableStateListOf<WellnessTask>().apply { addAll(getWellnessTasks()) }
}
- 通过移除列表的默认值来修改
WellnessTaskList
可组合函数,因为列表会提升到屏幕级别。添加一个新的lambda函数参数onCloseTask
(用于接收WellnessTask
以进行删除)。将onCloseTask
传递给WellnessTaskItem
。
您还需要进行一次更改。item
方法会接收一个key
参数。默认情况下,每个项的状态均与该项在列表中的位置相对应。
在可变列表中,当数据集发生变化时,这会导致问题,因为实际改变位置的项会丢失任何记住的状态。
使用每个WellnessTaskItem
的id
作为每个项的键,即可轻松解决此问题。
WellnessTaskList
将如下所示:
@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函数作为参数添加到有状态WellnessTaskItem
中并进行调用。
@Composable
fun WellnessTaskItem(
taskName: String,
onClise: () -> Unit,
modifier: Modifier = Modifier
) {
var checkedState by rememberSaveable { mutableStateOf(false) }
WellnessTaskItem(
taskName = taskName,
checked = checkedState,
onCheckedChange = { newValue -> checkedState = newValue },
onClose = onClose,
modifier = modifier
)
}
太棒了!此功能已经完成,现在已经可以从列表项中删除项。
如果您点击每行中的X,则事件会一直到达拥有状态的列表,从列表中删除相应项,并导致Compose重组界面。
如果您尝试使用rememberSaveable()
将列表存储在WellnessScreen
中,则会发生运行时异常。
cannot be saved using the current SaveableStateRegistry. The default implementation only support types which can be stored inside the Bundle. Please consideer implementing a custom Saver for this class and pass it to rememberSaveable().
次错误消息指出,您需要提供自定义Saver。但是,您不应该使用rememberSaveable
来存储需要长时间序列化或反序列化操作的大量数据或复杂数据结构。
使用activity的onSaveInstanceState
时,应遵循类似的规则。
十一、ViewModel中的状态
屏幕或及诶面状态指示应在屏幕上显示的内容(例如任务列表)。该状态通常会与层次结构中的其他层相关联,原因是其包含应用数据。
界面状态描述屏幕上显示的内容,而应用逻辑则描述应用的行为方式以及应如何响应状态变化。逻辑分为两种类型:第一种是界面行为或界面逻辑你,第二种是业务逻辑。
- 界面逻辑涉及如何在屏幕上显示状态变化(例如导航逻辑或显示信息提示控件)。
- 业务逻辑决定如何处理状态更改(例如付款或存储用户偏好设置)。该逻辑通常位于业务层或数据层,但绝不会位于界面层。
ViewModel
提供界面状态以及对位于其他层中的叶落逻辑的访问。此外,ViewModel还会在配置更改后继续保留,因此其生命周期比组合更长。ViewModel可以遵循Compose内容(即activity或fragment)的主机的生命周期,也可以遵循导航图的目的地的生命周期(如果您使用的是Compose Naviggation库)
警告:ViewModel并不是组合的一部分。因此,您不应该保留可组合项中创建的状态(例如,记住的值),因为这可能会导致内存泄漏。
11.1、迁移列表并移除方法
让我们将界面状态(列表)迁移到ViewModel,并开始将业务逻辑提取到ViewModel中。
- 创建文件
WellnessViewModel.kt
以添加ViewModel类。
将“数据源”getWellnnessTasks()
移至WellnessViewModel
。
像前面一样使用toMutableStateList
定义内部_tasks
变量,并将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) {
_task.remove(item)
}
}
private fun getWellnessTasks() = List(30) { i -> WellnessTask(i, "Task # $i") }
- 我们可以通过调用
viewModel()
函数,从任何可组合项访问此ViewModel。
如需使用此函数,请打开app/build.gradle
文件,添加以下库,并在Android Studio中同步新的依赖项:
implementation "androidx.lifecycle:lifecycle-viewmodel-compose:{latest_version}"
您可以点击此处查看最新版本。
- 打开
WellnessScreen
。实例化wellnessViewModel
ViewModel,方法是以Screen可组合项的参加的形式调用viewModel()
,以便在测试你此可组合项时进行替换,并根据需要进行提升。为WellnessTaskList
提供任务列表,并为onCloseTask
lambda提供remove函数。
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中使用了可组合项,则在该activity完成或进程终止之前,viewMoedl()
会返回同一实例。
大功告成!您已将ViewModel与部分状态和业务逻辑集成到了屏幕上。由于状态保留在组合之外并由ViewModel存储,因此对列表的更改在配置更改后继续有效。
ViewModel在任何情况下(例如,对于系统发起的进程终止)都不会自动保留应用的状态。
建议将ViewModel用于屏幕级可组合项,即靠近从导航图的activity、fragment或目的地调用的根可组合项。绝不应将ViewModel传递给其他可组合项,而是应当仅向它们传递所需的数据以及以参数形式执行所需逻辑的函数。
11.2、迁移选中状态
最后一个重构是将选中状态和逻辑迁移到ViewModel。这样一来,代码将变得更简单且更易于测试,并且所有状态均由ViewModel管理。
- 首先,修改
WellnessTask
模型类,使其能够存储选中状态并将false设置为默认值。
data class WellnessTask(val id: Int, val label: String, var checked: Boolean = false)
- 在ViewModel中,实现一个
changeTaskChecked
方法,该方法将接收使用选中状态的新值进行修改的任务。
class VellnessView: 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)
}
)
}
}
警告:将ViewModel实例传递给其他可组合项是一种不好的做法。您应仅传递它们数据以及将所需逻辑作为参数来执行的函数。
4. 打开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
文件。我们不再需要有状态方法,因为CheckBox状态将提升到列表级别。该文件仅包含以下可组合函数:
@Composable
fun WellnessTaskItem(
taskName: String,
checked: Boolean,
onCheckedChange: (Boolean) -> Unit,
onClose: () -> Unit,
modifier: Modifier = Modifier
) {
Row(
modifier = modifier, verticalAlignment = Alignment.CenterVertially
) {
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将跟踪MutableLiist
与添加和移除元素相关的更改。这就是删除功能能够正常运行的原因。但是,它对行项的值(在本例中为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
在构造函数中接收默认值为false
的initialChecked
变量,然后可以使用工厂方法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)
}
大功告成!这解决方案行之有效,并且所有更改在重组和配置更改后仍然保持有效!
11.3、测试
现在,业务逻辑已重构为ViewModel,而不是在可组合函数内形成耦合,因为单元测试要简单得多。
十二、恭喜
太棒了!!!