I am trying to implement drag-and-drop reordering in a LazyColumn in a Compose app. I am using a ViewModel and a Room database to store the values. While I have successfully implemented the drag-and-drop feature, the items in the list are being swapped arbitrarily, and their positions change unexpectedly after swapping two items. Any help would be appreciated.
Here is the screenshot and the important parts of the code:
# Composable
@RequiresApi(Build.VERSION_CODES.O)
@OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class)
@Composable
fun WorkoutSessionScreen(
navController: NavController,
workoutSessionViewModel: WorkoutSessionViewModel = hiltViewModel(),
workoutId: Int,
) {
val uiSetsState = workoutSessionViewModel.uiSetsState.collectAsState()
Scaffold(
modifier = Modifier
................
}, ) { innerPadding ->
Column(
modifier = Modifier
.padding(innerPadding)
)
{
val enteredIndex = remember { mutableIntStateOf(-1) }
LazyColumn(
modifier = Modifier
.fillMaxSize()
.padding(start = 14.dp, end = 14.dp)
) {
itemsIndexed(items = uiSetsState.value,
key = { index, item ->
item.set.setId!!
}) { index, setItem ->
Row(
modifier = Modifier
.fillMaxWidth()
.wrapContentHeight()
) {
simpleDetailItem(modifier = Modifier
.then(
if (enteredIndex.intValue == index) {
Modifier.background(Color.Cyan)
} else Modifier.background(Color.White)
)
.dragAndDropTarget(
shouldStartDragAndDrop = {
true
},
target = object : DragAndDropTarget {
override fun onDrop(event: DragAndDropEvent): Boolean {
setItem.set.setId?.let {
workoutSessionViewModel.swapDroppedSet(
it
)
}
enteredIndex.intValue = -1
return true
}
override fun onEntered(event: DragAndDropEvent) {
super.onEntered(event)
enteredIndex.intValue = index
}
override fun onExited(event: DragAndDropEvent) {
super.onExited(event)
enteredIndex.intValue = -1
}
}
),
setItem.set,
setDraggedItem = {
workoutSessionViewModel.setDraggedItem(it)
})
}
}
}
}//Column
}//scaffold
}
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun simpleDetailItem(
modifier: Modifier,
set: Set,
setDraggedItem: (id: Long?) -> Unit
) {
Row(
modifier = modifier
.fillMaxWidth()
.padding(horizontal = 16.dp, vertical = 16.dp)
.height(80.dp)
.border(width = 2.dp, color = Color.Green)
.dragAndDropSource {
detectTapGestures(
onLongPress = {
startTransfer(
DragAndDropTransferData(
ClipData.newPlainText(
"image Url", "test"
)
)
)
setDraggedItem(set.setId)
},
onTap = {
}
)
},
) {
set.exerciseTitle?.let {
Text(
modifier = Modifier
.padding(horizontal = 8.dp, vertical = 8.dp),
text = it + " - " + set.setId + " - " + set.setOrder
)
}
}
}
ViewModel
@RequiresApi(Build.VERSION_CODES.O)
@HiltViewModel
class WorkoutSessionViewModel @Inject constructor(
private val setRepository: SetRepository,
private val exerciseRepository: ExerciseRepository,
private val savedStateHandle: SavedStateHandle,
) : ViewModel() {
private var _uiSetsState: StateFlow<List<SetWithMeasurements>> =
setRepository.getSetsOfWorkoutWithMeasurements(savedStateHandle[WORKOUT_ID]!!)
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5_000),
initialValue = emptyList(),
)
val uiSetsState: StateFlow<List<SetWithMeasurements>> = _uiSetsState
fun swapDroppedSet(droppedSetId: Long) {
CoroutineScope(Dispatchers.IO).launch {
if (draggedSetId != droppedSetId) {
setRepository.swapTwoSets(draggedSetId, droppedSetId)
}
draggedSetId = 0
}
}
fun setDraggedItem(setId: Long?) {
if (setId != null) {
draggedSetId = setId
}
}
Repository
suspend fun swapTwoSets(draggedSet: Long, droppedSet: Long) {
val setDragged = setDao.getSetBySetIdWithMeasurements(draggedSet)
val setDropped = setDao.getSetBySetIdWithMeasurements(droppedSet)
if (setDragged != null && setDropped != null) {
val temp = setDragged.set.setOrder
setDragged.set.setOrder = setDropped.set.setOrder
setDropped.set.setOrder = temp
setDao.upsert(setDropped.set)
setDao.upsert(setDragged.set)
}
}
fun getSetsOfWorkoutWithMeasurements(workoutId: Long): Flow<List<SetWithMeasurements>> {
return setDao.getSetsByWorkoutIdWithMeasurements(workoutId).map { sets ->
sets.filter { it.set.exerciseID != null }
.onEach { setsWithMeasure ->
setsWithMeasure.set.exerciseTitle =
exerciseDao.getExerciseById(setsWithMeasure.set.exerciseID)?.title
}
}
}
I have tried adding keys to lazycolumn. I have learned that adding keys prevent problems about item positions and orders but it did not change anything.