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
}
)
}
)
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, 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))
}
}
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),
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 key parameter in itemsIndexed() for proper composition tracking:
itemsIndexed(
items = items,
key = { _, item -> item.id } // Unique, stable identifier
) { index, item ->
// Your content
}
This ensures 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 → https://myougatheax.gumroad.com
Top comments (0)