DEV Community

myougaTheAxo
myougaTheAxo

Posted on

Drag & Drop in Compose: Reorderable Lists & Swipe to Delete

Drag & Drop in Compose: Reorderable Lists & Swipe to Delete

Building interactive, gesture-driven UIs is a hallmark of modern Android apps. Jetpack Compose makes implementing drag-and-drop interactions elegant and performant. This guide covers the essential patterns for reorderable lists and swipe-to-delete functionality.

Detecting Drag Gestures

The detectDragGesturesAfterLongPress modifier enables drag interactions after a long press:

var offsetX by remember { mutableFloatStateOf(0f) }
var offsetY by remember { mutableFloatStateOf(0f) }

Box(
    modifier = Modifier
        .size(100.dp)
        .background(Color.Blue)
        .pointerInput(Unit) {
            detectDragGesturesAfterLongPress(
                onDrag = { change, dragAmount ->
                    change.consume()
                    offsetX += dragAmount.x
                    offsetY += dragAmount.y
                },
                onDragEnd = {
                    // Handle drag completion
                }
            )
        }
)
Enter fullscreen mode Exit fullscreen mode

GraphicsLayer for Visual Feedback

Provide real-time visual feedback during drag operations using graphicsLayer:

Box(
    modifier = Modifier
        .size(100.dp)
        .background(Color.Blue)
        .graphicsLayer {
            translationX = offsetX
            translationY = offsetY
            scaleX = if (isDragging) 1.1f else 1f
            scaleY = if (isDragging) 1.1f else 1f
        }
        .pointerInput(Unit) {
            detectDragGesturesAfterLongPress(
                onDragStart = {
                    isDragging = true
                },
                onDrag = { change, dragAmount ->
                    change.consume()
                    offsetX += dragAmount.x
                    offsetY += dragAmount.y
                },
                onDragEnd = {
                    isDragging = false
                    // Animate back to original position
                }
            )
        }
)
Enter fullscreen mode Exit fullscreen mode

Drag Handle Icon

Use a drag handle icon to signal that an item is draggable:

@Composable
fun DraggableListItem(
    item: String,
    onDrag: () -> Unit,
    modifier: Modifier = Modifier
) {
    Row(
        modifier = modifier
            .fillMaxWidth()
            .padding(16.dp)
            .pointerInput(Unit) {
                detectDragGesturesAfterLongPress(
                    onDragStart = onDrag,
                    onDrag = { change, dragAmount ->
                        change.consume()
                    }
                )
            },
        verticalAlignment = Alignment.CenterVertically
    ) {
        Icon(
            imageVector = Icons.Default.DragHandle,
            contentDescription = "Drag to reorder",
            modifier = Modifier
                .padding(end = 8.dp)
                .alpha(0.6f)
        )
        Text(item, modifier = Modifier.weight(1f))
    }
}
Enter fullscreen mode Exit fullscreen mode

SwipeToDismissBox for Swipe-to-Delete

The SwipeToDismissBox composable provides a swipe-to-dismiss pattern with customizable background content:

@Composable
fun SwipeDeleteItem(
    item: String,
    onDelete: () -> Unit,
    modifier: Modifier = Modifier
) {
    SwipeToDismissBox(
        modifier = modifier,
        backgroundContent = {
            Box(
                modifier = Modifier
                    .fillMaxSize()
                    .background(Color.Red)
                    .padding(16.dp),
                contentAlignment = Alignment.CenterEnd
            ) {
                Icon(
                    imageVector = Icons.Default.Delete,
                    contentDescription = "Delete",
                    tint = Color.White
                )
            }
        },
        content = {
            ListItem(
                headlineContent = { Text(item) },
                modifier = Modifier.background(Color.White)
            )
        },
        onDismissed = {
            onDelete()
        }
    )
}
Enter fullscreen mode Exit fullscreen mode

Complete Reorderable List

Combine these patterns for a full reorderable list with swipe-to-delete:

@Composable
fun ReorderableListScreen() {
    var items by remember { mutableStateOf(listOf("Item 1", "Item 2", "Item 3")) }
    var draggedItem by remember { mutableStateOf<String?>(null) }

    LazyColumn(
        modifier = Modifier.fillMaxSize()
    ) {
        itemsIndexed(
            items = items,
            key = { _, item -> item }
        ) { index, item ->
            SwipeToDismissBox(
                modifier = Modifier
                    .animateItem(
                        fadeInSpec = tween(300),
                        fadeOutSpec = tween(300),
                        placementSpec = spring()
                    )
                    .pointerInput(Unit) {
                        detectDragGesturesAfterLongPress(
                            onDragStart = {
                                draggedItem = item
                            },
                            onDrag = { change, _ ->
                                change.consume()
                            },
                            onDragEnd = {
                                draggedItem = null
                            }
                        )
                    },
                backgroundContent = {
                    Box(
                        modifier = Modifier
                            .fillMaxSize()
                            .background(Color.Red),
                        contentAlignment = Alignment.CenterEnd
                    ) {
                        Icon(Icons.Default.Delete, contentDescription = null)
                    }
                },
                content = {
                    Row(
                        modifier = Modifier
                            .fillMaxWidth()
                            .background(Color.White)
                            .padding(16.dp),
                        verticalAlignment = Alignment.CenterVertically
                    ) {
                        Icon(Icons.Default.DragHandle, contentDescription = null)
                        Text(item, modifier = Modifier.weight(1f))
                    }
                },
                onDismissed = {
                    items = items.filterNot { it == item }
                }
            )
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Performance Optimization

Use key parameter in itemsIndexed() for proper composition tracking:

itemsIndexed(
    items = items,
    key = { _, item -> item.id }  // Unique, stable identifier
) { index, item ->
    // Your content
}
Enter fullscreen mode Exit fullscreen mode

This ensures animations and state management work correctly during list reordering.

Best Practices

  1. Use Stable Keys: Always provide unique, stable identifiers for list items
  2. Visual Feedback: Show scale/alpha changes during drag to enhance UX
  3. Accessibility: Label drag handles and deletion actions clearly
  4. Performance: Avoid recomposing non-dragged items unnecessarily
  5. Animations: Use animateItem() for smooth list transitions

Jetpack Compose's gesture detection and modifier system make it straightforward to build sophisticated, interactive lists that feel responsive and polished.


8 Android App Templates → https://myougatheax.gumroad.com

Top comments (0)