I have a list of items divided into two categories...
# compose
e
I have a list of items divided into two categories, selected and unselected, and I have a button on each item that allows you to move the item between categories. I also have the option to order the items using drag and drop. I'm having an issue with handling the state: If I use a
mutableStateOf
to store the list, the drag and drop works, but the button to move the list items doesn't. If I use the list directly on the LazyColumn, the button to move the items works, but the drag and drop don't. Is there a way for the state to handle both situations correctly? Code on thread
Copy code
@Composable
fun Screen(
    list: ImmutableList<Item>,
    onItemAddedOrRemoved: (item: Item) -> Unit,
) {
    var stateList by remember { mutableStateOf(list) }
    val listState = rememberLazyListState()
    val dragDropState =
        rememberDragDropState(listState) { from, to ->
            stateList = stateList.toMutableList().apply {
                val fromIndex = indexOfFirst { it.id == from.key }
                val toIndex = indexOfFirst { it.id == to.key }
                add(toIndex, removeAt(fromIndex))
            }.toImmutableList()
        }

    LazyColumn(
        state = listState,
    ) {
        itemsIndexed(stateList, key = { _, item -> item.id }) { index, item ->
z
what's the code for the buttons?
e
The Image to move the item:
Copy code
Image(
    painter = painterResource(R.drawable.icn_minus),
    contentDescription = null,
    modifier = Modifier
        .size(32.dp)
        .clickable {
            onItemAddedOrRemoved(item)
        }
)
The Image to drag and drop:
Copy code
Image(
    painter = painterResource(R.drawable.icn_reorder),
    contentDescription = null,
    modifier = Modifier
        .size(32.dp)
        .dragContainer(dragDropState, item.id)
)
z
ah, so you
clickable
fires a callback out to the caller of your
Screen
composable, but doesn't update
stateList
at all.
👍 1
and if your caller passes in a new
list
, it's a no-op since
stateList
is only initialized from
list
on the first composition
👍 1
the biggest issue here looks like multiple sources of truth. It looks like your caller owns the list state, but then you make a copy aka a second source of truth, and the bug happens because the two sources diverge
e
I agree I have two sources of truth. As the list index is not part of the state, I couldn't think of a solution. Is there a way to "sync" the states?
z
I would first try to eliminate the duplicate sot: Probably the simplest way to do this is to could expand the
onItemAddedOrRemoved
callback to support all the modification operations you need, and then just rely on the caller to correctly update the list, and get rid of
stateList
. Using index to drive the operations could be tricky, since if multiple updates happen on the same frame the indices would be out of date, so i would recommend using IDs instead.
👍 1
💯 1
e
The API/examples for drag and drop only use the composable state to handle this. I can try to unify the state. Do you think having the state on the VM and updating it will keep the drag-and-drop working smoothly? https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:compose/[…]ndroidx/compose/foundation/demos/LazyColumnDragAndDropDemo.kt
s
Yes, it should still be performant. We use drag and drop with room/paging and propagate ordering changes all the way to the db and it's still snappy
❤️ 1