【问题标题】:Jetpack Compose: Row of "AndroidView" with dynamic listJetpack Compose:带有动态列表的“AndroidView”行
【发布时间】:2021-08-18 13:13:14
【问题描述】:

在 ViewModel 中,我维护一个字符串列表。如果将执行 onAddTagonRemoveTag,则 mutableState 将被刷新。这会导致重组。

问题是,如果我使用onRemoveTag 删除一个元素(例如第三个),更新的列表会到达我的“自定义可组合”(已调试)中,但渲染只会从列表中删除最后一项。

例子:

  • 初始列表:1,2,3,4 -> onRemove(2) -> 更新列表:1,3,4 -> 渲染:1,2,3
  • 初始列表:1,2,3,4 -> onRemove(1) -> 更新列表:2,3,4 -> 渲染:1,2,3

LazyListScope.items 的文档中有这个key 参数说:

key - 代表项目的稳定且唯一的密钥工厂。不允许对列表中的多个项目使用相同的键。密钥的类型应该可以通过 Android 上的 Bundle 保存。如果传递了 null,则列表中的位置将代表键。当您指定键时,滚动位置将基于该键保持,这意味着如果您在当前可见项之前添加/删除项目,则具有给定键的项目将保留为第一个可见项。 p>

也许Row 做了类似的事情,它无法正确决定哪个元素需要删除,哪个元素只保留位置信息。但是如何解决呢?

我不想在这里使用LazyRow,因为我只有几个项目要渲染。除此之外,我想了解这个问题! :)

代码

视图模型


class MyViewModel : ViewModel() {
   var selectedTags = mutableStateOf(listOf<String>())

   fun onRemoveTag(tag: String) {
      selectedTags.value = selectedTags.value.toMutableList().apply { remove(tag) }
   }

   fun onAddTag(tag: String) {
      selectedTags.value = selectedTags.value.toMutableList().apply { add(tag) }
   }
}
  

主屏幕

fun MainScreen(viewModel: MyViewModel) {
   val selectedTags by remember { viewModel.selectedTags }

   TagRow(tags = selectedTags)
}

自定义组合

@Composable
fun TagRow(
    tags: List<String>,
    modifier: Modifier = Modifier
) {
    Row(modifier = modifier) {
        tags.forEach {
            Text(text = it)
        }
    }
}

希望有人知道如何解决这个问题!

问候, 克里斯

编辑

根据@Philip 的反馈我准备了一个独立的例子:

@Composable
fun StackOverflowPreview() {
    val tags = remember { mutableStateListOf("1", "2", "3") }

    Row {
        tags.forEach {
            AndroidView(factory = { context ->
                EmojiTextView(context).apply {
                    textAlignment = View.TEXT_ALIGNMENT_CENTER
                    setTextColor(android.graphics.Color.BLACK)
                    layoutParams =
                        LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT)
                    text = it
                }
            })
        }
    }

    LaunchedEffect(key1 = tags) {
        delay(2000)
        tags.remove("1")
        
        delay(2000)
        tags.remove("2")
        
        delay(2000)
        tags.remove("3")
    }
}

在这里你可以看到我使用了EmojiTextView。如果我用基本的Text 可组合替换该视图。它按预期工作。使用EmojiTextView,项目将始终从右到左移除。

编辑 2

我能够将我的问题缩小到 AndroidView 集成:

@Preview
@Composable
fun StackOverflowPreview() {
    val tags = remember { mutableStateListOf("1", "2", "3") }

    Row {
        tags.forEach { tag ->
            // does NOT work
            // AndroidView(factory = { TextView(it).apply { text = tag } })

            // works
            AndroidView(factory = { TextView(it) }, update = { v -> v.text = tag })
        }
    }

    LaunchedEffect(key1 = tags) {
        delay(2000)
        tags.remove("1")

        delay(2000)
        tags.remove("2")

        delay(2000)
        tags.remove("3")
    }
}

【问题讨论】:

  • 如果您希望您的问题得到解答,您需要让专家尽快获得具有可重现问题的工作样本。完美地,我应该将您的代码粘贴到我的示例项目中,并在我运行它时尽快看到问题。不确定TagChip 是什么,但使用Text,您的代码完全可以正常工作。请将其更新为minimal reproducible example。你可以用LaunchedEffect模拟用户动作,比如:加1,2,3,4,延迟1秒,删除2,这样我们就可以很容易看到问题了
  • 您也可以将mutableStateOf(listOf&lt;String&gt;()) 替换为mutableStateListOf&lt;String&gt;() 以更轻松地处理修改。
  • 那么你的问题到底是什么? AndroidView factory 只被调用一次,并在任何下一次重组中重用。 update 在每次重组时都会被调用,这就是您需要更新 AndroidView 中的任何状态更改的方式
  • 我的问题是一个移动的目标! :) 实现 AndroidView 集成的工作原理解决了我的问题。谢谢

标签: android kotlin android-jetpack-compose


【解决方案1】:

MainScreen 可组合中,您将val 分配与remember 包装在一起。它会导致列表不会在重组时更新,因此请删除那里的remember。然后,您还在viewmodel 中的selectedTags 变量的初始化中使用了一种不太好的方法。您可以直接在其中使用委托,就像您在主要活动中使用的那样(或者看起来是可组合的)。在您的视图模型中,您可以将变量初始化为val selectedTags by mutableStateOf(listOf&lt;String&gt;()),然后就无需在任何地方使用.value 前缀。请放心,仍然会触发重组。您只需要使用mutableStateOf() 进行初始化。

此外,从 Compose 1.0.1 开始,声明可变列表的更好方法是使用预定义的 mutableStateListOf(...),您因此不需要使用委托(“by”关键字),并且可以使用它就像一个普通的列表对象,没有任何.value 调用。

只要实现这些就可以了

【讨论】:

  • 感谢有关状态使用的提示。我一定会调查的。除此之外,认为这些问题是由我的使用 EmojiTextView 引起的(请参阅编辑)
  • 我认为EmojiTextView 不会有问题。
【解决方案2】:

在这里完成这个线程是一个工作示例。

@Preview
@Composable
fun StackOverflowPreview() {
    val tags = remember { mutableStateListOf("1", "2", "3") }

    Row {
        tags.forEach { tag ->
            AndroidView(factory = { TextView(it) }, update = { v -> v.text = tag })
        }
    }

    LaunchedEffect(key1 = tags) {
        delay(2000)
        tags.remove("1")

        delay(2000)
        tags.remove("2")

        delay(2000)
        tags.remove("3")
    }
}

正如@Philip 在 cmets 中提到的那样,AndroidView 只会被实例化一次,并且需要一种方法来在重组时更新其内部状态。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2022-10-25
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多