Reputation: 639
In a ViewModel I maintain a list of strings. If onAddTag
or onRemoveTag
will be performed a mutableState will be refresh. Which causes a recomposition.
The problem is that if I remove a element (eg the third) with onRemoveTag
the updated list arrivies in my "Custom Composable" (debugged) but the rendering only removes the last item from the list.
Example:
In the Documentation of LazyListScope.items
there is this key
parameter which says:
key - a factory of stable and unique keys representing the item. Using the same key for multiple items in the list is not allowed. Type of the key should be saveable via Bundle on Android. If null is passed the position in the list will represent the key. When you specify the key the scroll position will be maintained based on the key, which means if you add/remove items before the current visible item the item with the given key will be kept as the first visible one.
Maybe the Row
does similar things and it can't properly decide which element needs to be remove and which one stays with the positional information only. But how to fix that?
I do not want do use a LazyRow
here because I only have a few items to render. Additional to that I want to understand the problem! :)
ViewModel
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) }
}
}
MainScreen
fun MainScreen(viewModel: MyViewModel) {
val selectedTags by remember { viewModel.selectedTags }
TagRow(tags = selectedTags)
}
Custom Composable
@Composable
fun TagRow(
tags: List<String>,
modifier: Modifier = Modifier
) {
Row(modifier = modifier) {
tags.forEach {
Text(text = it)
}
}
}
Hope somebody knows how to fix that!
Regards, Chris
According to @Philip's feedback I prepared a self-contained example:
@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")
}
}
Here you can see that I use EmojiTextView
. If I replace that view with a basic Text
composable. It works like expected. With the EmojiTextView
the items will always remove from right to left.
I was able to narrow down my problem to the AndroidView-integration:
@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")
}
}
Upvotes: 3
Views: 3994
Reputation: 639
To finish up this thread here is an working example.
@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")
}
}
As @Philip mentioned in the comments the AndroidView will be just instantiated once and needs a way to update its internal state on recomposition.
Upvotes: 0
Reputation: 6863
In the MainScreen
Composable, you are wrapping the val
assignment with remember
. It causes the list to not be updated on recompositions, so remove that remember
over there. Then, you have also used a not-so-good approach in the initialization of the selectedTags
variable in the viewmodel
. You can directly use delegation inside that, as you have used in your main activity (or composable, as it seems). In your viewmodel, you can initialise the variable as val selectedTags by mutableStateOf(listOf<String>())
, and then there's no need for using the .value
prefix everywhere. Rest assured that recompositions will be triggered still. You just need to initialise with mutableStateOf()
.
Also, a better approach of declaring mutable lists, as of Compose 1.0.1, is to use the pre-defined mutableStateListOf(...)
with which you so not require to use delegation (the 'by' keyword), and can use it just like a regular list object, without any .value
calls.
Just implement these and you should be fine
Upvotes: 0