aclassen / ComposeReorderable

Enables reordering by drag and drop in Jetpack Compose (Desktop) LazyList & LazyGrid.
Apache License 2.0
836 stars 89 forks source link

No support for changing lists in canDragOver? #269

Open CalamityDeadshot opened 1 year ago

CalamityDeadshot commented 1 year ago

I have a list of selectable items and I want to be able to reorder only selected items. So I have a composable like this:

@Composable
private fun EmployeeSelectionDialog(
    employees: List<EmployeeEntity>,
    selectedEmployees: List<EmployeeEntity>,
    onDismissRequest: () -> Unit,
    onClick: (EmployeeEntity, Boolean) -> Unit,
    onReordered: (List<EmployeeEntity>) -> Unit
) {

    var selectedEmployeesMutable = remember(selectedEmployees) {
        println("Selected employees: ${selectedEmployees.joinToString { it.id.toString() }}")
        selectedEmployees
    }

    val reorderState = rememberReorderableLazyListState(
        onMove = { from, to ->
            selectedEmployeesMutable = selectedEmployeesMutable.toMutableList().apply {
                add(to.index, removeAt(from.index))
            }
        },
        canDragOver = { draggedOver: ItemPosition, _: ItemPosition ->
            (draggedOver.key in selectedEmployeesMutable.map { it.id }).also {
                println("Can drag over ${draggedOver.key}: $it (against [${selectedEmployeesMutable.joinToString { it.id.toString() }}])")
            }
        },
        onDragEnd = { _, _ ->
            onReordered(selectedEmployeesMutable)
        }
    )

    val unselectedEmployees by remember(employees, selectedEmployees) {
        mutableStateOf(employees.filter { it !in selectedEmployees })
    }

    @Composable
    fun LazyItemScope.EmployeeItem(
        employee: EmployeeEntity,
        isDragging: Boolean,
    ) {
        // Composing what I need and applying the detectReorder modifier if selected
    }

    LazyColumn(
        modifier = Modifier
            .width(400.dp)
            .reorderable(reorderState),
        state = reorderState.listState
    ) {
        items(
            items = selectedEmployeesMutable,
            key = { it.id }
        ) { employee ->
            ReorderableItem(reorderState, key = employee.id) { isDragging ->
                EmployeeItem(
                    employee = employee,
                    isDragging = isDragging
                )
            }
        }
        if (selectedEmployees.isNotEmpty()) {
            item {
                Divider()
            }
        }
        items(
            items = unselectedEmployees,
            key = { it.id }
        ) { employee ->
            EmployeeItem(
                employee = employee,
                isDragging = false
            )
        }
    }
  }
}

onClick and onReordered functions end up making a DB write to mark items selected or change their order respectively, and this data then gets passed back into composition through Flows.

The problem is that the canDragOver factory seems to capture what it needs in a closure and be remembered along with its ReorderableLazyListState, not caring that selectedEmployeesMutable was changed. So if I check some checkboxes and then try to reorder selected items, my console will look like this:

Selected employees: 
Selected employees: 42
Selected employees: 42, 26
Can drag over 26: false (against [])
Can drag over 26: false (against [])

This issue is even worse for un-checking some items since selectedEmployeesMutable captured in a closure retains wrong items, and this can cause IndexOutOfBoundsExceptions.

So are my assumptions correct and can I fix this on my end, or is the fix needed in the library? Thanks.

OliverRhyme commented 1 year ago

did you try using rememberUpdatedState?