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

Published: (March 1, 2026 at 08:19 PM EST)
4 min read
Source: Dev.to

Source: Dev.to

myougaTheAxo

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
                }
            )
        }
)

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
                }
            )
        }
)

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, _ -> 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))
    }
}

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() }
    )
}

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)
                            .padding(16.dp),
                        contentAlignment = Alignment.CenterEnd
                    ) {
                        Icon(
                            imageVector = Icons.Default.Delete,
                            contentDescription = "Delete",
                            tint = Color.White
                        )
                    }
                },
                content = {
                    ListItem(
                        headlineContent = { Text(item) },
                        modifier = Modifier.background(
                            if (draggedItem == item) Color.LightGray else Color.White
                        )
                    )
                },
                onDismissed = {
                    items = items.toMutableList().also { it.remove(item) }
                }
            )
        }
    }
}

Swipe‑to‑Dismiss & Drag‑to‑Reorder Example

@Composable
fun SwipeAndDragList() {
    var items by remember { mutableStateOf(listOf("Item 1", "Item 2", "Item 3")) }

    LazyColumn {
        itemsIndexed(
            items = items,
            key = { _, item -> item }
        ) { index, item ->
            SwipeToDismiss(
                state = rememberDismissState(),
                background = {
                    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 }
                }
            )
        }
    }
}

Performance Optimization

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

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

Providing a stable key ensures that animations and state management work correctly during list reordering.

Best Practices

  • Use Stable Keys – Always provide unique, stable identifiers for list items.
  • Visual Feedback – Show scale/alpha changes during drag to enhance UX.
  • Accessibility – Label drag handles and deletion actions clearly.
  • Performance – Avoid recomposing non‑dragged items unnecessarily.
  • 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 →

0 views
Back to Blog

Related posts

Read more »