Jetpack Compose(第七趴)——Jetpack Compose 中的状态(下)

六、组合中的记忆功能

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函数会接收任务说明和onCloselambda函数(就像内置Button可组合项接收onClick一样)。

WellnessTaskItem如下所示:

image.png

接下来为应用添加更多功能,请更新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")
        }
    }
}

使用WellnessTaskItemonCloselambda函数实现:在按下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")
            }
        }
    }
}

运行应用时,屏幕会显示初始状态:

image.png

右侧是简化版组件树,可帮助您分析状态发生变化时会发生什么情况。countshowTask是记住的值。

现在,您可以在应用中按以下步骤操作:

  • 按下Add one按钮。此操作会递增count(这会导致重组),并同时显示WellnessTaskItem和计数器Text

image.png

  • 按下WellnessTaskItem组件的X(这会导致另一项重组)。showTask现在为false,这意味着不再显示WellnessTaskItem

image.png

  • 按下Add one按钮(另一项重组)。如果您继续增加杯数,showTask会记住您在下一次重组时关闭了WellnessTaskItem

image.png

  • 按下Clear water count按钮可将count重置为0并导致重组。系统不会调用显示countText以及与WellnessTaskItem相关的代码,并且会退出组合。

image.png

  • 由于系统未调用之前调用showTask的代码位置,因此会忘记showTask。这将返回第一步。

image.png

  • 按下Add one按钮,使count大于0(重组)。

image.png

  • 系统再次显示WellnessTaskItem可组合项,因为在退出上述组合时,之前的showTask值已被忘记。

如果我们要求showTaskcount重置为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后消失。

3.gif

如果您更改语言、在深色模式与浅色模式之间切换,或者执行任何导致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) }
    ...
}

现在运行应用并尝试进行一些配置更改。您应该会看到计数器已正确保存。

2.gif

重新创建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)
}

太棒了!您已将countStatelessCounter提升到StatefulCounter
您可以将其插入到应用中,并使用StatefulCounter更新WellnessScreen:

@Composable








fun WellnesScreen(modifier: Modifier = Modifier) {
    StatefulCounter(modifier)
}

要点:提升状态时,有三条规则可帮您弄清楚状态应去向何处:

  1. 状态应至少提升到使用该状态(读取)的所有可组合项的最低共同父项
  2. 状态应至少提升到它可以发生变化(写入)的最高级别
  3. 如果两种状态发生变化以响应相同的事件,它们应提升到同一级别

您可以将状态提升到高于这些规则要求的级别,但如果未将状态提升到足够高的级别,则遵循单向数据流会变得困难或不可能。

如前所述,状态提升具有一些好处。我们将探索此代码的不同变体并详细介绍其中一些变体。

  1. 您的无状态可组合项现在已可重复使用

如需记录饮水和果汁的杯数,请记住waterCountjuiceCount,但请使用示例StatelessCounter可组合函数来显示两种不同的独立状态。

@Composable








fun StatefulCounter() {

    var waterCount by remember { mutableStateOf(0) }
    var juiceCount by remember { mutableStateOf(0) }
    StatelessCounter(waterCount, { waterCounter++ })
    StatelessCounter(juiceCount, { juiceCount++ })
}


image.png

如果修改了juiceCount,则重组StatefulCounter。在重组期间,Compose会识别哪些函数读取juiceCount,并触发系统仅重组这些函数。

image.png

当用户点按以递增juiceCount时,系统会重组StatefulCounter,同时也会重组juiceCountStatelessCounter。但不会重组读取waterCountStatelessCounter

image.png

  1. 有状态可组合函数可以为多个可组合函数提供相同的状态。
@Composable








fun StatefulCounter() {

    var count by remember { mutableStateOf(0) }
    
    StatelessCounter(count, { count++ })
    AnotherStatelessMethod(count, {count *= 2 })
}


在本例中,如果通过StatelessCounterAnotherStatelessMethod更新计数,则系统会按预期重组所有项目。

由于可以共享提升的状态,因此请务必仅传递可组合项所需的状态,以避免不必要的重组并提高可重用性。

要点:设计可组合项的最佳实践仅向它们传递所需要的参数。

九、使用列表

接下来,添加应用的第二项功能,即健康任务列表。您可以对列表中的项执行以下两项操作:

  • 勾选列表项,将任务标记为已完成。
  • 从任务列表中移除不想完成的任务。

9.1、设置

  1. 首先,修改列表项。您可以重复使用“组合中的记忆功能”部分中的WellnessTaskItem,并将其更新为包含Checkbox。请务必提升checked状态和onCheckedChange回调,使函数变为无状态。

image.png

本部分的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")
        }
    }
}
  1. 在同一文件中,添加一个有状态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,
    )
}
  1. 创建一个文件WellnessTask.kt,对包含ID和标签的任务进行建模。将其定义为数据类。
data class WellnessTask(val id: Int, val label: String)
  1. 对于任务列表本身,请创建一个名为WellnessTasksList.kt的新文件,并添加一个方法用于生成一些虚假数据:
private fun getWellnessTasks() = List(30) { i -> WellnessTask(i, "Task # $i") }

请注意,在真实应用中,您将从数据层获取数据。

  1. 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)
        }
    }

}

  1. 将列表添加到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.运行应用并试一下效果!现在,您应该能够勾选任务,但不能删除任务。

1.gif

9.2、在LazyList中恢复项状态

现在,我们来详细了解一下WellnessTaskItem可组合项中的一些内容。

checkedState属于每个WellnessTaskItem可组合项,就像私有变量一样。当checkedState发生变化时,系统只会重绘WellnessTaskItem的实例,而不是重组LazyColumn中的所有WellnessTaskItem实例。

请按以下步骤尝试应用的功能:

  1. 勾选此列表项部的所有元素(例如元素1和元素2)
  2. 滚动到列表底部,使这些元素位于屏幕之外。
  3. 滚动到顶部,查看之前勾选的列表项。
  4. 请注意,它们处于未选中状态。

正如您在上一部分中看到的那样,其问题在于,当一个项退出组合时,系统会忘记之前记住的状态。对于LazyColumn上的项,当年您滚动至项不可见的位置时,这些不可见的项会完全退出组合。

d3c12f57cc98db16.gif

如何解决此问题?同样,使用rememberSaveable。它采用保存的实例状态机制,可确保存储的值在重新创建activity或进程之后继续保留。得益于rememberSaveableLazyList配合工作的方式,您的项在离开组合后也能继续保留。

只需在有状态WellnessTaskItem中将remember替换为rememberSaveable即可,如下所示:

import androidx.compose.runtime.saveable.rememberSaveable








var checkedState by rememberSaveable { mutableStateOf(false) }

9b4ba365481ae97a.gif

9.3、Compose中的常见模式

请注意LazyColumn的实现:

@Composable








fun lazyColumn(
    ...
    state: LazyListState = rememberLazyListState(),
    ...

)

可组合函数rememberLazyListState使用rememberSaveable为列表创建初始状态。重新创建activity后,无需任何编码即可保持滚动状态。

许多应用更需要对滚动位置、列表项布局更改以及其他与列表状态相关的事件作出响应,并进行监听。延迟组件(例如LazyColumnLazyRow)可通过提升LazyListState来支持用例。

状态参数使用由公共rememberX函数提供的默认值时内置可组合函数中的常见模式。

十、可观察的可变列表

接下来,如需添加从列表中移除任务的行为,第一步是让列表成为可变列表。

使用可变对象(例如ArrayList<T>mutableListOf),对此不起作用。这些类型不会向Compose通知列表中的项已发生更改并安排界面重组。您需要使用其他API。

您需要创建一个可由Compose观察的MutableList实例。此结构可允许Compose跟踪更改,以便在列表中添加或移除项时重组界面。

首先,定义可观察MutableList。扩展函数toMutableStateList()用于根据初始可变或不可变的Collection(例如List)来创建可观察的MutableList

或者,您也可以使用工厂方法mutableStateListOf来创建可观察的MutableList,然后为初始状态添加元素。

mutableStateOf函数会返回一个类型为MutableState<T>的对象。
mutableStateListOf和toMutableStateList函数会返回一个类型为SnapshotStateList<T>的对象。
  1. 打开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()) }
}

  1. 通过移除列表的默认值来修改WellnessTaskList可组合函数,因为列表会提升到屏幕级别。添加一个新的lambda函数参数onCloseTask(用于接收WellnessTask以进行删除)。将onCloseTask传递给WellnessTaskItem

您还需要进行一次更改。item方法会接收一个key参数。默认情况下,每个项的状态均与该项在列表中的位置相对应。

在可变列表中,当数据集发生变化时,这会导致问题,因为实际改变位置的项会丢失任何记住的状态。

使用每个WellnessTaskItemid作为每个项的键,即可轻松解决此问题。

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) }
        }
    }
}
  1. 修改WellnessTaskItem:将onCloselambda函数作为参数添加到有状态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重组界面。

image.png

如果您尝试使用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中。

  1. 创建文件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") }
  1. 我们可以通过调用viewModel()函数,从任何可组合项访问此ViewModel。

如需使用此函数,请打开app/build.gradle文件,添加以下库,并在Android Studio中同步新的依赖项:

implementation "androidx.lifecycle:lifecycle-viewmodel-compose:{latest_version}"

您可以点击此处查看最新版本。

  1. 打开WellnessScreen。实例化wellnessViewModelViewModel,方法是以Screen可组合项的参加的形式调用viewModel(),以便在测试你此可组合项时进行替换,并根据需要进行提升。为WellnessTaskList提供任务列表,并为onCloseTasklambda提供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管理。

  1. 首先,修改WellnessTask模型类,使其能够存储选中状态并将false设置为默认值。
data class WellnessTask(val id: Int, val label: String, var checked: Boolean = false)
  1. 在ViewModel中,实现一个changeTaskChecked方法,该方法将接收使用选中状态的新值进行修改的任务。
class VellnessView: ViewModel() {
    ...
    fun changeTaskChecked(item: WellnessTask, checked: Boolean) =
        tasks.find { it.id == item.id }?.let { task ->
            task.checked = checked
        }

}


  1. 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并添加onCheckedTasklambda函数参数,以便将其传递给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) }
            )
        }
    }
}
  1. 清理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")
        }
    }
}
  1. 运行应用并尝试勾选任何任务。您会发现无法勾选任何任务。

8e2e731c58123cd8.gif

这是因为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在构造函数中接收默认值为falseinitialChecked变量,然后可以使用工厂方法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)
}

大功告成!这解决方案行之有效,并且所有更改在重组和配置更改后仍然保持有效!

c8e049d56e7b0eac.gif

11.3、测试

现在,业务逻辑已重构为ViewModel,而不是在可组合函数内形成耦合,因为单元测试要简单得多。

十二、恭喜

太棒了!!!

© 版权声明
THE END
喜欢就支持一下吧
点赞0

Warning: mysqli_query(): (HY000/3): Error writing file '/tmp/MYIFJOPY' (Errcode: 28 - No space left on device) in /www/wwwroot/583.cn/wp-includes/class-wpdb.php on line 2345
admin的头像-五八三
评论 抢沙发
头像
欢迎您留下宝贵的见解!
提交
头像

昵称

图形验证码
取消
昵称代码图片