androidandroid-jetpack-composemutablelist

Mutable state of list doesn't update correctly


I'm new in Jetpack Compose, I'm trying to do a drag and drop screen with items to be moved from a column to another one. I took inspiration from this link and I creted the following files:

TaskColumns.kt

@Preview
@Composable
fun TaskColumns() {

    var columnWidth by remember {
        mutableStateOf(0.dp)
    }

    val density = LocalDensity.current

   val items = remember {
       mutableListOf(
           mutableListOf(5,6,7,8),
           mutableListOf(),
           mutableListOf(),
           mutableListOf())
   }

    Surface(
        Modifier.fillMaxSize()
    ) {
        LongPressDraggable {
            Row(Modifier.padding(4.dp)) {
                repeat(4){
                    val column = it
                    DropTarget<IntArray>(modifier = Modifier.weight(1f)) { a, data ->
                        val n = data?.get(0)
                        val i = data?.get(1)
                        val c = data?.get(2)


                        var color = if (a) Color.Green
                        else Color.Blue

                        if (i != null && n != null && c != null) {
                            if (c != column){
                                Log.d("DND", "${data[0]}, ${data[1]}")
                                items[column].add(n)
                                items[c].remove(n)
                            }
                            color = Color.Blue
                        }
                        Column(
                            Modifier
                                .fillMaxHeight()
                                .fillMaxWidth()
                                .padding(4.dp)
                                .background(color)
                                .onPlaced {
                                    columnWidth = with(density) { it.size.width.toDp() }
                                }
                        ) {
                            items[column].forEachIndexed { index, item ->
                                DragTarget(
                                    modifier = Modifier,
                                    dataToDrop = intArrayOf(item, index, column),
                                    onDragCustomAction = {
                                        Log.d("DND", "$item")
                                    }
                                ) {
                                    Column(
                                        Modifier
                                            .size(columnWidth)
                                            .aspectRatio(1f / 1f)
                                            .background(Color.Black),
                                        horizontalAlignment = Alignment.CenterHorizontally,
                                        verticalArrangement = Arrangement.Center
                                    ) {
                                        Text(text = "$item", color = Color.White)
                                    }
                                }
                                Spacer(modifier = Modifier.height(8.dp))
                            }
                        }
                    }

                }
            }
        }
    }
}

DragAndDrop.kt

internal val LocalDragTargetInfo = compositionLocalOf { DragTargetInfo() }

@Composable
fun LongPressDraggable(
    modifier: Modifier = Modifier,
    content: @Composable BoxScope.() -> Unit
) {
    val state = remember { DragTargetInfo() }
    CompositionLocalProvider(
        LocalDragTargetInfo provides state
    ) {
        Box(modifier = modifier.fillMaxSize(), contentAlignment = Alignment.TopStart)
        {
            content()
            if (state.isDragging) {
                var targetSize by remember {
                    mutableStateOf(IntSize.Zero)
                }
                Box(modifier = Modifier
                    .graphicsLayer {
                        val offset = (state.dragPosition + state.dragOffset)
//                        scaleX = 1.3f
//                        scaleY = 1.3f
                        alpha = if (targetSize == IntSize.Zero) 0f else .9f
                        translationX = offset.x.minus(targetSize.width / 2)
                        translationY = offset.y.minus(targetSize.height / 2)
                    }
                    .onGloballyPositioned {
                        targetSize = it.size
                    }
                ) {
                    state.draggableComposable?.invoke()
                }
            }
        }
    }
}

@Composable
fun <T> DragTarget(
    modifier: Modifier,
    dataToDrop: T,
    onDragCustomAction: ()->Unit = {},
    onDragCancelCustomAction: ()->Unit = {},
    content: @Composable (() -> Unit),
) {

    var currentPosition by remember { mutableStateOf(Offset.Zero) }
    val currentState = LocalDragTargetInfo.current

    Box(modifier = modifier
        .onGloballyPositioned {
            currentPosition = it.localToWindow(Offset.Zero)
        }
        .pointerInput(Unit) {
            detectDragGesturesAfterLongPress(onDragStart = {
                currentState.dataToDrop = dataToDrop
                currentState.isDragging = true
                currentState.dragPosition = currentPosition + it
                currentState.draggableComposable = content
            }, onDrag = { change, dragAmount ->
                change.consume()
                currentState.dragOffset += Offset(dragAmount.x, dragAmount.y)
                onDragCustomAction()
            }, onDragEnd = {
                currentState.isDragging = false
                currentState.dragOffset = Offset.Zero
            }, onDragCancel = {
                onDragCancelCustomAction()
                currentState.dragOffset = Offset.Zero
                currentState.isDragging = false

            })
        }) {
        content()
    }
}

@Composable
fun <T> DropTarget(
    modifier: Modifier,
    content: @Composable() (BoxScope.(isInBound: Boolean, data: T?) -> Unit)
) {

    val dragInfo = LocalDragTargetInfo.current
    val dragPosition = dragInfo.dragPosition
    val dragOffset = dragInfo.dragOffset
    var isCurrentDropTarget by remember {
        mutableStateOf(false)
    }

    Box(modifier = modifier.onGloballyPositioned {
        it.boundsInWindow().let { rect ->
            isCurrentDropTarget = rect.contains(dragPosition + dragOffset)
        }
    }) {
        val data =
            if (isCurrentDropTarget && !dragInfo.isDragging) dragInfo.dataToDrop as T? else null
        content(isCurrentDropTarget, data)
    }
}

internal class DragTargetInfo {
    var isDragging: Boolean by mutableStateOf(false)
    var dragPosition by mutableStateOf(Offset.Zero)
    var dragOffset by mutableStateOf(Offset.Zero)
    var draggableComposable by mutableStateOf<(@Composable () -> Unit)?>(null)
    var dataToDrop by mutableStateOf<Any?>(null)
}

With this code I should be able to move a black square from a column to the other one. The problem is that if I move the first item for example, it works, but then if I try to move the second one (that now became the first in the list) the value displayed is the same of the previous item.

You can see an example in this gif:

Animated example

The problem seems to be the value passed to dataToDrop in DragTarget doesn't update. What am I doing wrong?


Solution

  • I suspect that the error occurs as a result of the way how you store the items data structure. In the official documentation they say:

    Caution:
    Using mutable objects such as ArrayList<T> or mutableListOf() as state in Compose causes your users to see incorrect or stale data in your app. Mutable objects that are not observable, such as ArrayList or a mutable data class, are not observable by Compose and don't trigger a recomposition when they change.

    Please try to use a mutableStateListOf() instead:

    val items = remember {
        mutableStateListOf(
            mutableStateListOf(5,6,7,8),
            mutableStateListOf(),
            mutableStateListOf(),
            mutableStateListOf()
        )
    }
    

    Additionally, you need to define a key for the DragTarget Composables like so:

    items[column].forEachIndexed { index, item ->
    
        key(item) {
            DragTarget(
                //...
            ) {
                //...
            }
        }
    }