I want to make some item swap mechanism in LazyColumn items. For this I have used itemIndexed but after a swipe, index numbers becomes the same. For example if I swap first item with second the item index for both become 0. I suspect this behavior because of reuse of component in LazyColumn.
@Composable
fun Screen(modifier:Modifier = Modifier) {
val itemList = remember { mutableStateListOf("Item 1", "Item 2", "Item 3", "Item 4") }
val context = LocalContext.current
LazyColumn(modifier = modifier) {
itemsIndexed(itemList, key = { _, text -> text }) { index, text ->
Card(
modifier = Modifier
.pointerInput(Unit) {
detectTapGestures(
onTap = {
Toast.makeText(context, "" + index, Toast.LENGTH_SHORT).show()
},
onDoubleTap = {
Collections.swap(itemList, index, index + 1)
}
)
}
.padding(8.dp)
.fillMaxWidth()
.animateItem()
) {
Row(
modifier = Modifier.padding(16.dp).fillMaxSize()
) {
Text("$index. $text")
}
}
}
}
}
In this example I can tap to look index number of the item and double tap on a item to swap with bottom item.
Test : If I click on first item it toast 0 and second item toast 1. Now when I double tap on the first item or second item and then do the same both toast 0. But the Text displaying correct indexes in both cases.
I came up with solution but it is very inefficient nor feels the right way. Which is declaring variable index inside items and reassign it before the Text with the index for itemIndexed (notice the lambda parameter index change to i). But multiple recompsitions happens this.
@Composable
fun Screen(modifier:Modifier = Modifier) {
...
LazyColumn(modifier = modifier) {
itemsIndexed(...) { i/*index change to i*/, text ->
// New Index declaration
var index by remember { mutableIntStateOf(i) }
Card(...) {
Row(
modifier = Modifier.padding(16.dp).fillMaxSize()
) {
index = i // Reassigns here
Text("$index. $text")
}
}
}
}
}
Any help is appreciated.
The pointerInput
's lambda is the problem. It captures the itemList
object and the index
object. When you reorder the list, the lambda stays the same because the key you provide for pointerInput
didn't change. You provide Unit
, and that is always the same. From the documentation:
The pointer input handling
block
will be cancelled and re-started whenpointerInput
is recomposed with a differentkey1
[...].
That means the captured objects of the lambda won't update. For itemList
that isn't an issue because only the content of the list is modified, but it is still the same list object. That's different for the captured index
objects, they don't match the current index
anymore.
For composables that provide key parameters so the lambda provided during the previous composition isn't touched (like remember
, LaunchedEffect
and also pointerInput
), it is important to provide all objects as keys that should still lead to an updated lambda. You should check each outside variable you access inside your lambda if it is important that it is updated. In your case you access these variables:
You always want the lambda to access the same index that itemsIndexed
provides, so you must pass this as a key to pointerInput
.
You also need to access the current itemList, but that actually never changes (it is declared as val
, not var
, and the list is remembered, so it stays the same object on each recomposition; only its content is modified). It wouldn't be wrong to also pass it as a key, but you don't need to.
Toast.LENGTH_SHORT is a constant (more precise, it's the Java equivalent, a static final
variable), so this can never change and shouldn't be passed as a key.
The context actually can change in some situations. Although not relevant to your current problem, you should actually pass that as a key too.
It should look like this:
.pointerInput(context, index) {
detectTapGestures(
onTap = {
Toast.makeText(context, "" + index, Toast.LENGTH_SHORT).show()
},
onDoubleTap = {
Collections.swap(itemList, index, index + 1)
}
)
}
And that's it, the pointerInput
lambda is now updated when needed so you can always access the correct index
.