问题与在Jetpack Compose中删除对象有关。

huangapple go评论99阅读模式
英文:

Problem with deleting object in jetpack compose

问题

我有一个屏幕,在其中我创建了5个可拖动的对象。一切都应该正常工作。您可以拖动它们并将它们放在屏幕的任何位置。以下是我的MainActivity:

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            GraggableItemsTheme {

                val viewModel: MainViewModel by viewModels()

                val list = viewModel.scoreData.collectAsState()
                // 使用主题中的'background'颜色的表面容器
                Surface(
                    modifier = Modifier.fillMaxSize(),
                    color = MaterialTheme.colorScheme.background
                ) {
                    list.value?.forEach {  i ->
                        DraggableTextLowLevel(
                            id = i,
                            onDelete = viewModel::deleteItem
                        )
                    }

                }
            }
        }
    }
}

@Composable
private fun DraggableTextLowLevel(
    id: Int,
    onDelete: (Int) -> Unit
) {
    Box(modifier = Modifier.fillMaxSize()) {
        var offsetX by remember { mutableStateOf(0f) }
        var offsetY by remember { mutableStateOf(0f) }

        Box(
            Modifier
                .offset {
                    IntOffset(
                        offsetX.roundToInt(),
                        offsetY.roundToInt()
                    )
                }
                .background(Color.Blue)
                .size(50.dp)
                .pointerInput(Unit) {
                    detectTapGestures(
                        onTap = {
                            onDelete(id)
                        }
                    )
                }
                .pointerInput(Unit) {
                    detectDragGestures { change, dragAmount ->
                        change.consume()
                        offsetX += dragAmount.x
                        offsetY += dragAmount.y
                    }
                }
        ) {
            Text(text = "$id")
        }
    }
}

ViewModel:

class MainViewModel : ViewModel() {

private val _scoreData = MutableStateFlow<List<Item>?>(
    listOf(
        Item(10,"one"),
        Item(20,"two"),
        Item(30,"three"),
        Item(40,"four"),
        Item(50,"five")
    )
)
val scoreData: StateFlow<List<Item>?> =
    _scoreData.asStateFlow()

fun deleteItem(number: Int) {
    println(number.toString())
    _scoreData.value = _scoreData.value?.toMutableStateList().also {
        println("要删除的项 $number")
        val itemToDelete = it?.find { item ->
            item.id == number
        }
        try {
            it?.remove(itemToDelete)
            println("成功")
        }
        catch (e: Exception) {
            println(e.toString())
        }

    }
}

数据类Item:

data class Item(
    val id: Int = 0,
    val name: String
)

问题是,当我单击要删除的项时,界面删除了错误的项。我已经尝试了我知道的一切,但没有结果。有时它会删除预期的项,但在大多数情况下,它会删除错误的项。有人可以帮我吗?我卡了几天了!

英文:

i have this screen in which i create 5 draggable objects. Everything should be working as expected. You can drag them and place them everywhere on the screen. Here is my MainActivity:

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            GraggableItemsTheme {

                val viewModel: MainViewModel by viewModels()

                val list = viewModel.scoreData.collectAsState()
                // A surface container using the &#39;background&#39; color from the theme
                Surface(
                    modifier = Modifier.fillMaxSize(),
                    color = MaterialTheme.colorScheme.background
                ) {
                    list.value?.forEach {  i -&gt;
                        DraggableTextLowLevel(
                            id = i,
                            onDelete = viewModel::deleteItem
                        )
                    }

                }
            }
        }
    }
}

@Composable
private fun DraggableTextLowLevel(
    id: Int,
    onDelete: (Int) -&gt; Unit
) {
    Box(modifier = Modifier.fillMaxSize()) {
        var offsetX by remember { mutableStateOf(0f) }
        var offsetY by remember { mutableStateOf(0f) }

        Box(
            Modifier
                .offset {
                    IntOffset(
                        offsetX.roundToInt(),
                        offsetY.roundToInt()
                    )
                }
                .background(Color.Blue)
                .size(50.dp)
                .pointerInput(Unit) {
                    detectTapGestures(
                        onTap = {
                            onDelete(id)
                        }
                    )
                }
                .pointerInput(Unit) {
                    detectDragGestures { change, dragAmount -&gt;
                        change.consume()
                        offsetX += dragAmount.x
                        offsetY += dragAmount.y
                    }
                }
        ) {
            Text(text = &quot;$id&quot;)
        }
    }
}

and the viewModel:

class MainViewModel : ViewModel() {

private val _scoreData = MutableStateFlow&lt;List&lt;Item&gt;?&gt;(
    listOf(
        Item(10,&quot;one&quot;),
        Item(20,&quot;two&quot;),
        Item(30,&quot;three&quot;),
        Item(40,&quot;four&quot;),
        Item(50,&quot;five&quot;)
    )
)
val scoreData: StateFlow&lt;List&lt;Item&gt;?&gt; =
    _scoreData.asStateFlow()

fun deleteItem(number: Int) {
    println(number.toString())
    _scoreData.value = _scoreData.value?.toMutableStateList().also {
        println(&quot;Item to delete $number&quot;)
        val itemToDelete = it?.find { item -&gt;
            item.id == number
        }
        try {
            it?.remove(itemToDelete)
            println(&quot;success&quot;)
        }
        catch (e: Exception) {
            println(e.toString())
        }

    }
}

and the data class of item:

data class Item(
    val id: Int = 0,
    val name: String
)

The problem is that when i click an item to delete ui deletes wrong item. I have tried everything i know but no result.Sometimes it erases the expected item but most of the cases it deletes the wrong. Can someone help me plz? I am stuck for days!

答案1

得分: 3

I will only provide translations for the non-code parts of the text you provided:

"这是因为有些东西很难找到并且很难处理。

这与所有循环都存在的系统性问题相同,即项目按组合顺序使用,因此如果数据在集合中移动得很多,将执行比严格必要的更多重新组合。例如,如果在集合的开头插入一个项目,整个视图将需要重新组合,而不仅仅是创建第一个项目并重用其余项目而不更改。这可能会导致用户界面混淆,例如,如果在项目块中使用了带有选择的输入字段,则选择将不会跟踪数据值,而将跟踪集合中的索引顺序。如果用户在第一个项目中选择文本,插入新项目将导致选择出现在新项目中选择似乎是随机文本,并且选择将丢失在用户可能期望仍然具有选择的项目中。

基本上,当您更改项目位置时,它们的状态与其位置不匹配,Compose会因为基于位置而不是数据保留节点而感到困惑。

解决此问题的最简单方法是添加映射到数据的“key”。

val viewModel: MainViewModel by viewModels()
    
val list = viewModel.scoreData.collectAsState()

Surface(
    modifier = Modifier.fillMaxSize(),
) {
    list.value?.forEach { item ->
        key(
            item.id
        ) {
            DraggableTextLowLevel(
                id = item.id,
                onDelete = viewModel::deleteItem
            )
        }
    }
}

您的代码还可以简化为在收集流时添加项目的一个SnapshotStateList。

另一种解决方法是尽量减少创建带有状态的可组合对象。如果将状态提升,最新的值将作为参数传递。

编辑

而较少为人知但很酷的一种存储可组合对象的状态的方法,即使它们退出组合,也可以使用movableContentOf

@Composable
private fun StatefulCheckBox(title: String, isChecked: Boolean) {
    Row(
        verticalAlignment = Alignment.CenterVertically
    ) {

        var checked by remember {
            mutableStateOf(isChecked)
        }
        Text(text = title)
        Checkbox(checked = checked,
            onCheckedChange = {
                checked = it
            }
        )
    }
}

通过默认删除项目,结果是错误的状态位于最新索引,如gif所示。通过使用movableStateOf,状态得以保留。

[![enter image description here][1]][1]

@Composable
private fun StatefulListSample() {
    val list = remember {
        mutableStateListOf(
            Item("Item1", checked = false),
            Item("Item2", checked = true),
            Item("Item3", checked = true),
        )
    }

    Button(onClick = {
        list.removeFirstOrNull()
    }) {
        Text(text = "Delete First Item")
    }

    list.forEach { item ->
        StatefulCheckBox(item.title, item.checked)
    }
}

@Composable
private fun MovableContentOfSample() {
    val list = remember {
        mutableStateListOf(
            Item("Item1", checked = false),
            Item("Item2", checked = true),
            Item("Item3", checked = true),
        )
    }

    val movableItems =
        list.map { item ->
            movableContentOf {
                StatefulCheckBox(item.title, item.checked)
            }
        }

    Button(onClick = {
        list.removeFirstOrNull()
    }) {
        Text(text = "Delete First Item")
    }

    list.forEachIndexed { index, item ->
        movableItems[index]()
    }
}

来源

https://newsletter.jorgecastillo.dev/p/movablecontentof-and-movablecontentwithreceivero

https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:compose/runtime/design/movable-content.md


Please note that this translation includes both the text and code comments in the original content.

<details>
<summary>英文:</summary>

This is because something sneaky and hard to come by.

&gt; This has the same systemic problem all for loops have in that the
&gt; items are used in order of composition so if data moves around in the
&gt; collections a lot more recomposition is performed than is strictly
&gt; necessary. For example, if one item was inserted at the beginning of
&gt; the collection the entire view would need to be recomposed instead of
&gt; just the first item being created and the rest being reused
&gt; unmodified. This can cause the UI to become confused if, for example,
&gt; input fields with selection are used in the item block as the
&gt; selection will not track with the data value but with the index order
&gt; in the collection. If the user selected text in the first item,
&gt; inserting a new item will cause selection to appear in the new item
&gt; selecting what appears to be random text and the selection will be
&gt; lost in the item the user might have expected to still have selection.

Basically, when you change item positions their state don&#39;t match their positions and Compose gets confused because it keeps nodes based on position instead of data.

The simplest way to solve this is to add `key` that maps to data.

    val viewModel: MainViewModel by viewModels()
    
    val list = viewModel.scoreData.collectAsState()
    // A surface container using the &#39;background&#39; color from the theme
    Surface(
        modifier = Modifier.fillMaxSize(),
    ) {
        list.value?.forEach { item -&gt;
            key(
                item.id
            ) {
                DraggableTextLowLevel(
                    id = item.id,
                    onDelete = viewModel::deleteItem
                )
            }
        }
    }

Your code also can be simplified to have one SnapshotStateList that you add items when you collect flow.

Another way to solve this to not create Composables with state whenever possible. If you hoist state the latest values will passed as parameters.

&lt;h2&gt;Edit&lt;/h2&gt;

And less known but cool way to store states of Composable even if they **exit composition** is to use `movableContentOf`.

    @Composable
    private fun StatefulCheckBox(title: String, isChecked: Boolean) {
        Row(
            verticalAlignment = Alignment.CenterVertically
        ) {
    
            var checked by remember {
                mutableStateOf(isChecked)
            }
            Text(text = title)
            Checkbox(checked = checked,
                onCheckedChange = {
                    checked = it
                }
            )
        }
    }


    data class Item(val title: String, val checked: Boolean)

Removing items by default results wrong state in latest index as can be seen in gif. By using movableStateOf state is preserved.

[![enter image description here][1]][1]


    @Composable
    private fun StatefulListSample() {
        val list = remember {
            mutableStateListOf(
                Item(&quot;Item1&quot;, checked = false),
                Item(&quot;Item2&quot;, checked = true),
                Item(&quot;Item3&quot;, checked = true),
            )
        }
    
        Button(onClick = {
            list.removeFirstOrNull()
        }) {
            Text(text = &quot;Delete First Item&quot;)
        }
    
        list.forEach { item -&gt;
            StatefulCheckBox(item.title, item.checked)
        }
    }
    
    @Composable
    private fun MovableContentOfSample() {
        val list = remember {
            mutableStateListOf(
                Item(&quot;Item1&quot;, checked = false),
                Item(&quot;Item2&quot;, checked = true),
                Item(&quot;Item3&quot;, checked = true),
            )
        }
    
        val movableItems =
            list.map { item -&gt;
                movableContentOf {
                    StatefulCheckBox(item.title, item.checked)
                }
            }
    
        Button(onClick = {
            list.removeFirstOrNull()
        }) {
            Text(text = &quot;Delete First Item&quot;)
        }
    
        list.forEachIndexed { index, item -&gt;
            movableItems[index]()
        }
    }


Sources

https://newsletter.jorgecastillo.dev/p/movablecontentof-and-movablecontentwithreceivero

https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:compose/runtime/design/movable-content.md


  [1]: https://i.stack.imgur.com/ihTJ7.gif

</details>



# 答案2
**得分**: 0

以下是您要翻译的内容:

问题在于您将可拖动对象的ID传递给`deleteItem()`函数,但该ID与列表中对象的索引不同。

要解决此问题,您需要在调用`deleteItem()`函数之前获取列表中对象的索引。您可以使用`indexOf()`函数来实现这一点。

以下代码将解决此问题:

```kotlin
class MainViewModel : ViewModel() {

    private var _list = mutableStateListOf(1, 2, 3, 4, 5)
    val list: List<Int> = _list

    private val _scoreData = MutableStateFlow<List<Int>?>(listOf(1,2,3,4,5))
    val scoreData: StateFlow<List<Int>?> =
        _scoreData.asStateFlow()

    fun deleteItem(number: Int) {
        println(number.toString())
        val index = _scoreData.value?.indexOf(number)
        _scoreData.value = _scoreData.value?.toMutableStateList().also {
            it?.removeAt(index)
        }
    }
}
英文:

The problem is that you are passing the id of the draggable object to the deleteItem() function, but the id is not the same as the index of the object in the list.

To fix this, you need to get the index of the object in the list before you call the deleteItem() function. You can do this by using the indexOf() function.

The following code will fix the problem:

class MainViewModel : ViewModel() {

    private var _list = mutableStateListOf(1, 2, 3, 4, 5)
    val list: List&lt;Int&gt; = _list

    private val _scoreData = MutableStateFlow&lt;List&lt;Int&gt;?&gt;(listOf(1,2,3,4,5))
    val scoreData: StateFlow&lt;List&lt;Int&gt;?&gt; =
        _scoreData.asStateFlow()

    fun deleteItem(number: Int) {
        println(number.toString())
        val index = _scoreData.value?.indexOf(number)
        _scoreData.value = _scoreData.value?.toMutableStateList().also {
            it?.removeAt(index)
        }
    }
}

huangapple
  • 本文由 发表于 2023年5月14日 03:15:24
  • 转载请务必保留本文链接:https://go.coder-hub.com/76244495.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定