I am developing a test application with Jetpack Compose using drag and drop functionalities. I have 3 tasks that are in each assigned column: To Do, In Progress, and Done. When I try to move any task to another column, it changes but then returns to the initial column. What could be the issue?
Here is my code on GitHub: GitHub Repository
internal val LocalDragTargetIfo = compositionLocalOf{ DragTargetInfo() }
@Composable
fun DragableScreen(modifier: Modifier = Modifier,content: @Composable BoxScope.() -> Unit){
val state= remember {
DragTargetInfo()
}
CompositionLocalProvider(LocalDragTargetIfo provides state){
Box(modifier=Modifier.fillMaxSize()){
content()
if(state.isDagging){
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> DragTrarget(
modifier: Modifier = Modifier,
datatoDrop: T,
viewModel: MainViewModel,
content: @Composable () -> Unit
) {
var currentPosition by remember { mutableStateOf(Offset.Zero) }
val currentState = LocalDragTargetIfo.current
Box(
modifier = modifier
.onGloballyPositioned { currentPosition = it.localToWindow(Offset.Zero) }
.pointerInput(Unit) {
detectDragGesturesAfterLongPress(
onDragStart = {
viewModel.startDragging()
currentState.dataToDrop = datatoDrop
currentState.isDagging = true
currentState.dragPosition = currentPosition + it
currentState.draggableComposable = content
},
onDrag = { change, dragAmount ->
change.consume()
currentState.dragOffset += Offset(dragAmount.x, dragAmount.y)
},
onDragEnd = {
viewModel.stopDragging()
currentState.dragOffset = Offset.Zero
currentState.isDagging = false
},
onDragCancel = {
viewModel.stopDragging()
currentState.dragOffset = Offset.Zero
currentState.isDagging = false
}
)
}
) {
content()
}
}
@Composable
fun <T> DropItem(
modifier: Modifier,
content: @Composable BoxScope.(isInBound: Boolean, data: T?) -> Unit
) {
val dragInfo = LocalDragTargetIfo.current
val dragPosition = dragInfo.dragPosition
val dragOffset = dragInfo.dragOffset
var isCurretDropTarget by remember { mutableStateOf(false) }
Box(
modifier = modifier
.background(Color.Red)
.onGloballyPositioned {
it.boundsInWindow().let { rect ->
isCurretDropTarget = rect.contains(dragPosition + dragOffset)
}
}
) {
val data = if (isCurretDropTarget && !dragInfo.isDagging) {
dragInfo.dataToDrop as T?
} else {
null
}
// Solo ejecuta el contenido si el arrastre ha terminado y está en el área de soltar
if (!dragInfo.isDagging && isCurretDropTarget && data != null) {
content(true, data)
} else {
content(false, null)
}
}
}
internal class DragTargetInfo {
var isDagging: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)
}
class MainViewModel : ViewModel() {
var isCurrentlyDragging by mutableStateOf(false)
private set
val items = mutableStateListOf<Task>()
var addedTasks = mutableStateListOf<Task>()
private set
fun addExampleTasks() {
items.addAll(listOf(
Task(1, "Task 1","Description 1", TaskStatus.TO_DO),
Task(2, "Task 2","Description 2", TaskStatus.IN_PROGRESS),
Task(3, "Task 3","Description 3", TaskStatus.DONE)
))
}
fun startDragging() {
isCurrentlyDragging = true
}
fun stopDragging() {
isCurrentlyDragging = false
}
fun addTask(task: Task) {
items.add(task)
}
fun updateTaskStatus(task: Task, newStatus: TaskStatus) {
val index = items.indexOfFirst { it.id == task.id }
if (index != -1) {
val currentTask = items[index]
if (currentTask.status != newStatus) {
println("Updating task ${task.id} from ${currentTask.status} to $newStatus")
// Actualizar el estado directamente
items[index] = currentTask.copy(status = newStatus)
}
}
}
}
@Composable
fun MainScreen(mainViewModel: MainViewModel = viewModel()) {
// Observar directamente el estado de items
val tasks = mainViewModel.items
// Llamar a un método para añadir tareas de ejemplo (solo si es necesario)
LaunchedEffect(Unit) {
if (tasks.isEmpty()) {
mainViewModel.addExampleTasks()
}
}
Row(modifier = Modifier.fillMaxSize()) {
TaskColumn(
title = "To Do",
tasks = tasks.filter { it.status == TaskStatus.TO_DO },
onTaskDropped = { task ->
mainViewModel.updateTaskStatus(task, TaskStatus.TO_DO)
},
viewModel = mainViewModel,
modifier = Modifier.weight(1f)
)
TaskColumn(
title = "In Progress",
tasks = tasks.filter { it.status == TaskStatus.IN_PROGRESS },
onTaskDropped = { task ->
mainViewModel.updateTaskStatus(task, TaskStatus.IN_PROGRESS)
},
viewModel = mainViewModel,
modifier = Modifier.weight(1f)
)
TaskColumn(
title = "Done",
tasks = tasks.filter { it.status == TaskStatus.DONE },
onTaskDropped = { task ->
mainViewModel.updateTaskStatus(task, TaskStatus.DONE)
},
viewModel = mainViewModel,
modifier = Modifier.weight(1f)
)
}
}
@Composable
fun TaskColumn(
title: String,
tasks: List<Task>,
onTaskDropped: (Task) -> Unit,
viewModel: MainViewModel, // Asegúrate de recibir el ViewModel
modifier: Modifier = Modifier
) {
Column(
modifier = modifier
.fillMaxHeight()
.padding(8.dp)
.border(1.dp, Color.Black, shape = RoundedCornerShape(8.dp))
) {
Text(title, style = MaterialTheme.typography.bodyMedium, modifier = Modifier.padding(8.dp))
tasks.forEach { task ->
DragTrarget(
modifier = Modifier.wrapContentWidth().padding(4.dp),
datatoDrop = task,
viewModel = viewModel // Pasar el ViewModel aquí
) {
TaskItem(task)
}
}
DropItem<Task>(
modifier = Modifier.fillMaxSize()
) { isInBound, droppedTask ->
if (isInBound && droppedTask != null) {
println("Task dropped: ${droppedTask.title} to $title")
onTaskDropped(droppedTask)
}
}
}
}
@Composable
fun TaskItem(task: Task) {
Box(
modifier = Modifier
.wrapContentWidth()
.padding(8.dp)
.background(Color.LightGray, shape = RoundedCornerShape(8.dp)) // Aquí se aplica el radio
.border(1.dp, Color.LightGray, shape = RoundedCornerShape(8.dp)) // Asegúrate de aplicar el mismo shape al borde
.padding(8.dp)
) {
Column {
Text("${task.title}:")
Text("${task.description}")
}
}
}
data class Task (val id: Int, val title: String, val description: String, var status: TaskStatus)
enum class TaskStatus{
TO_DO,IN_PROGRESS,DONE
}
You don’t need to build drag and drop functionality from scratch in jetpack compose because it supports this natively.
I’ve updated your code to use compose drag and drop features and also made the code a bit simpler and more concise.
Here’s my version:
import android.content.ClipData
import android.content.ClipDescription
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.draganddrop.dragAndDropSource
import androidx.compose.foundation.draganddrop.dragAndDropTarget
import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.key
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draganddrop.DragAndDropEvent
import androidx.compose.ui.draganddrop.DragAndDropTarget
import androidx.compose.ui.draganddrop.DragAndDropTransferData
import androidx.compose.ui.draganddrop.mimeTypes
import androidx.compose.ui.draganddrop.toAndroidDragEvent
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.input.key.Key
import androidx.compose.ui.unit.dp
import androidx.lifecycle.ViewModel
import com.abdo21.mydragapp.ui.theme.MyDragAppTheme
import androidx.lifecycle.viewmodel.compose.viewModel
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContent {
Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
App(modifier = Modifier
.padding(innerPadding)
)
}
}
}
}
enum class QueueType(val title: String) {
TodoQueue("To Do"),
InProgressQueue("In Progress"),
DoneQueue("Done")
}
data class Task(
val id: Int,
val title: String,
val description: String
)
class MyViewModel: ViewModel() {
val todoTasks = mutableStateListOf<Task>(
Task(id = 1, title = "Task 1", description = "Description 1"),
Task(id = 2, title = "Task 4", description = "Description 4"),
)
val inProgressTasks = mutableStateListOf<Task>(
Task(id = 3, title = "Task 2", description = "Description 2"),
Task(id = 4, title = "Task 5", description = "Description 5"),
)
val doneTasks = mutableStateListOf<Task>(
Task(id = 5, title = "Task 3", description = "Description 3"),
)
fun moveTask(
task: Task,
from: QueueType,
to: QueueType
) {
when(from) {
QueueType.TodoQueue -> todoTasks.remove(task)
QueueType.InProgressQueue -> inProgressTasks.remove(task)
QueueType.DoneQueue -> doneTasks.remove(task)
}
when(to) {
QueueType.TodoQueue -> todoTasks.add(task)
QueueType.InProgressQueue -> inProgressTasks.add(task)
QueueType.DoneQueue -> doneTasks.add(task)
}
}
}
data class DraggedData(
val task: Task,
val originQueue: QueueType,
)
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun TaskCard(
task: Task,
queueType: QueueType,
modifier: Modifier = Modifier
) {
Column(
modifier = modifier
.dragAndDropSource {
detectTapGestures(
onLongPress = {
startTransfer(
DragAndDropTransferData(
ClipData.newPlainText("Task", "Task"),
localState = DraggedData(task, queueType)
)
)
}
)
}
.background(
color = Color.LightGray,
shape = RoundedCornerShape(8.dp)
)
.padding(vertical = 4.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
Text(text = task.title)
Text(text = task.description)
}
}
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun QueueTask(
queueType: QueueType,
tasks: List<Task>,
modifier: Modifier = Modifier,
onTaskDrop: (Task, QueueType) -> Unit
) {
val dropTarget = remember {
object : DragAndDropTarget {
override fun onDrop(dragStartEvent: DragAndDropEvent): Boolean {
val dropEvent = dragStartEvent.toAndroidDragEvent()
val draggedTaskData = dropEvent.localState as? DraggedData
val droppedTask = draggedTaskData?.task
droppedTask?.let { onTaskDrop(it, draggedTaskData.originQueue) }
return true
}
}
}
Column(
modifier = modifier
.dragAndDropTarget (
shouldStartDragAndDrop = { dragStartEvent ->
dragStartEvent.mimeTypes().contains(ClipDescription.MIMETYPE_TEXT_PLAIN)
}, target = dropTarget
)
.border(
width = 1.dp,
color = Color.Black,
shape = RoundedCornerShape(8.dp)
)
.fillMaxHeight(),
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(text = queueType.title)
Spacer(modifier = Modifier.height(4.dp))
tasks.forEach { task ->
key(task.id) {
TaskCard(
task = task,
queueType = queueType,
modifier = Modifier
.padding(vertical = 4.dp)
)
}
}
}
}
@Composable
fun App(
modifier: Modifier = Modifier,
viewModel: MyViewModel = viewModel()
) {
val todoTasks = viewModel.todoTasks
val inProgressTasks = viewModel.inProgressTasks
val doneTasks = viewModel.doneTasks
Row(
modifier = modifier,
horizontalArrangement = Arrangement.spacedBy(10.dp)
) {
QueueTask(
queueType = QueueType.TodoQueue,
tasks = todoTasks,
modifier = Modifier.weight(1f),
onTaskDrop = { task, fromQueueType ->
viewModel.moveTask(task, fromQueueType, QueueType.TodoQueue)
}
)
QueueTask(
queueType = QueueType.InProgressQueue,
tasks = inProgressTasks,
modifier = Modifier.weight(1f),
onTaskDrop = { task, fromQueueType ->
viewModel.moveTask(task, fromQueueType, QueueType.InProgressQueue)
}
)
QueueTask(
queueType = QueueType.DoneQueue,
tasks = doneTasks,
modifier = Modifier.weight(1f),
onTaskDrop = { task, fromQueueType ->
viewModel.moveTask(task, fromQueueType, QueueType.DoneQueue)
}
)
}
}
Demo: