六、组合中的回忆功用

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如下所示:

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

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

运转运用时,屏幕会显现初始状况:

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

右侧是简化版组件树,可协助您分析状况产生改动时会产生什么状况。countshowTask是记住的值。

现在,您可以在运用中按以下过程操作:

  • 按下Add one按钮。此操作会递加count(这会导致重组),并一起显现WellnessTaskItem和计数器Text

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

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

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

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

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

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

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

  • 由于体系未调用之前调用showTask的代码方位,因而会忘掉showTask。这将回来第一步。

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

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

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

  • 体系再次显现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后消失。

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

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

现在运转运用并测验进行一些装备更改。您应该会看到计数器已正保证存。

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

从头创立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++ })
}

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

假如修正了juiceCount,则重组StatefulCounter。在重组期间,Compose会辨认哪些函数读取juiceCount,并触发体系仅重组这些函数。

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

当用户点按以递加juiceCount时,体系会重组StatefulCounter,一起也会重组juiceCountStatelessCounter。但不会重组读取waterCountStatelessCounter

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

  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回调,使函数变为无状况。

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

本部分的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.运转运用并试一下效果!现在,您应该可以勾选使命,但不能删去使命。

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

9.2、在LazyList中康复项状况

现在,咱们来具体了解一下WellnessTaskItem可组合项中的一些内容。

checkedState归于每个WellnessTaskItem可组合项,就像私有变量相同。当checkedState产生改动时,体系只会重绘WellnessTaskItem的实例,而不是重组LazyColumn中的一切WellnessTaskItem实例。

请按以下过程测验运用的功用:

  1. 勾选此列表项部的一切元素(例如元素1和元素2)
  2. 翻滚到列表底部,使这些元素位于屏幕之外。
  3. 翻滚到顶部,检查之前勾选的列表项。
  4. 请留意,它们处于未选中状况。

正如您在上一部分中看到的那样,其问题在于,当一个项退出组合时,体系会忘掉之前记住的状况。关于LazyColumn上的项,当年您翻滚至项不行见的方位时,这些不行见的项会彻底退出组合。

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

怎么处理此问题?同样,运用rememberSaveable。它选用保存的实例状况机制,可保证存储的值在从头创立activity或进程之后持续保存。得益于rememberSaveableLazyList配合作业的办法,您的项在脱离组合后也能持续保存。

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

import androidx.compose.runtime.saveable.rememberSaveable
var checkedState by rememberSaveable { mutableStateOf(false) }

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

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重组界面。

Jetpack Compose(第七趴)——Jetpack 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中。

  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. 运转运用并测验勾选任何使命。您会发现无法勾选任何使命。

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

这是由于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)
}

功德圆满!这处理方案行之有用,而且一切更改在重组和装备更改后仍然坚持有用!

11.3、测验

现在,事务逻辑已重构为ViewModel,而不是在可组合函数内构成耦合,由于单元测验要简略得多。

十二、祝贺

太棒了!!!