Reorder List¶
Reordering builds on the core drag and drop primitives: the reorderableItem modifier makes an item both draggable and a drop target at the same time, which is all a sortable list needs.
Setting Up¶
Reordering uses the standard rememberDragAndDropState and DragAndDropContainer:
val dndState = rememberDragAndDropState<String>()
DragAndDropContainer(
state = dndState,
) {
// Reorderable items go here
}
See Drag and Drop Overview for the state and container parameters.
reorderableItem Modifier¶
Apply Modifier.reorderableItem to each item that can be reordered:
items(items, key = { it }) { item ->
val isDragging = dndState.isDragging(item)
ItemCard(
text = item,
modifier = Modifier
.graphicsLayer { alpha = if (isDragging) 0f else 1f }
.reorderableItem(
key = item,
data = item,
state = dndState,
onDragEnter = { state ->
items = items.toMutableList().apply {
val index = indexOf(item)
if (index != -1) {
remove(state.data)
add(index, state.data)
}
}
},
draggableContent = {
ItemCard(text = item, isDragShadow = true)
},
)
.fillMaxWidth(),
)
}
Parameters¶
| Parameter | Type | Default | Description |
|---|---|---|---|
key | Any | Required | Unique key identifying this item. |
data | T | Required | Data associated with this item. |
state | DragAndDropState<T> | Required | The drag and drop state. |
enabled | Boolean | true | Whether this item is reorderable. |
dragAfterLongPress | Boolean | Inherits from state | Override long-press drag behavior. |
requireFirstDownUnconsumed | Boolean | Inherits from state | Override unconsumed pointer requirement. |
dropTargets | List<Any> | emptyList() | Restrict which targets this item can be dropped on. |
dropStrategy | DropStrategy | DropStrategy.SurfacePercentage | Strategy for choosing the hovered target. |
dragAxis | DragAxis | DragAxis.Free | Constrain drag movement to one axis. See Axis Lock. |
hasDragHandle | Boolean | false | If true, drag is initiated from a dragHandle modifier. See Drag Handle. |
zIndex | Float | 0f | Z-index for overlapping items. |
dropAnimationSpec | AnimationSpec<Offset> | SpringSpec() | Animation for position when dropping. |
sizeDropAnimationSpec | AnimationSpec<Size> | SpringSpec() | Animation for size when dropping. |
dropAlignment | Alignment | Alignment.Center | Alignment of the dropped item within the target. |
dropOffset | Offset | Offset.Zero | Additional offset for the drop animation position. |
dropAnimationEnabled | Boolean | true | Whether to animate the drop. |
onDrop | (DraggedItemState<T>) -> Unit | {} | Called when an item is dropped on this target. |
onDragEnter | (DraggedItemState<T>) -> Unit | {} | Called when a dragged item enters this target. |
onDragExit | (DraggedItemState<T>) -> Unit | {} | Called when a dragged item exits this target. |
draggableContent | @Composable () -> Unit | Required | Content rendered as the drag shadow. |
Reorder Logic with onDragEnter¶
The reorder logic is implemented in the onDragEnter callback. When a dragged item enters another item's area, you update the list order:
onDragEnter = { state ->
items = items.toMutableList().apply {
val targetIndex = indexOf(item)
if (targetIndex != -1) {
remove(state.data)
add(targetIndex, state.data)
}
}
}
Note
The onDragEnter callback receives the DraggedItemState of the item being dragged. Use state.data to identify the dragged item and reposition it in your list.
Reorder Hysteresis¶
When two items of different sizes swap, the swap itself can move the target back under the cursor and immediately trigger the reverse swap, causing the two items to oscillate during a slow drag. Compose DND prevents this with a built-in hysteresis: after a swap, the cursor has to travel a minimum distance in the opposite direction before the just-swapped target can be re-entered.
The default distance is 8.dp and works out of the box. You can tune it on either state:
val dndState = rememberDragAndDropState<String>(
reorderHysteresisDistance = 16.dp, // 0.dp disables hysteresis
)
// or with the wrapper API state
val reorderState = rememberReorderState<String>(
reorderHysteresisDistance = 16.dp,
)
Observing Reorder State¶
You can observe the current drag state through DragAndDropState:
// Currently dragged item
val draggedItem = dndState.draggedItem
// Key of the drop target currently being hovered
val hoveredKey = dndState.hoveredDropTargetKey
// Whether a specific item is being dragged
val isDragging = dndState.isDragging(item)
Full Working Example with LazyColumn¶
@Composable
fun ReorderListExample() {
val dndState = rememberDragAndDropState<String>()
var items by remember {
mutableStateOf(
listOf("Item 1", "Item 2", "Item 3", "Item 4", "Item 5")
)
}
DragAndDropContainer(
state = dndState,
modifier = Modifier.fillMaxSize().padding(20.dp),
) {
LazyColumn(
verticalArrangement = Arrangement.spacedBy(12.dp),
modifier = Modifier.fillMaxSize(),
) {
items(items, key = { it }) { item ->
val isDragging = dndState.isDragging(item)
ItemCard(
text = item,
modifier = Modifier
.animateItem()
.graphicsLayer { alpha = if (isDragging) 0f else 1f }
.reorderableItem(
key = item,
data = item,
state = dndState,
onDragEnter = { state ->
items = items.toMutableList().apply {
val index = indexOf(item)
if (index != -1) {
remove(state.data)
add(index, state.data)
}
}
},
draggableContent = {
ItemCard(
text = item,
isDragShadow = true,
)
},
)
.fillMaxWidth(),
)
}
}
}
}
@Composable
private fun ItemCard(
text: String,
isDragShadow: Boolean = false,
modifier: Modifier = Modifier,
) {
Card(
modifier = modifier.height(60.dp),
elevation = CardDefaults.cardElevation(
defaultElevation = if (isDragShadow) 8.dp else 2.dp,
),
) {
Box(
contentAlignment = Alignment.CenterStart,
modifier = Modifier
.fillMaxSize()
.padding(horizontal = 16.dp),
) {
Text(text = text)
}
}
}
Tip
Use graphicsLayer { alpha = if (isDragging) 0f else 1f } to hide the original item while it is being dragged, so only the drag shadow is visible. Adding Modifier.animateItem() inside a LazyColumn animates the displacement of the other items.
Place animateItem() before reorderableItem in the modifier chain. This lets the library track the item's animated position, so dropping while items are still moving lands the drag shadow exactly where the item settles.
Alternative: Wrapper Composables¶
If you prefer wrapping composables over modifier chains, the reorder API is also available as ReorderContainer + ReorderableItem with a dedicated ReorderState:
val reorderState = rememberReorderState<String>()
ReorderContainer(
state = reorderState,
) {
LazyColumn {
items(items, key = { it }) { item ->
ReorderableItem(
state = reorderState,
key = item,
data = item,
onDrop = {},
onDragEnter = { state ->
items = items.toMutableList().apply {
val index = indexOf(item)
if (index == -1) return@ReorderableItem
remove(state.data)
add(index, state.data)
}
},
) {
// isDragging is available in this scope
Text(
text = item,
modifier = Modifier
.graphicsLayer {
alpha = if (isDragging) 0f else 1f
}
)
}
}
}
}
Differences from the modifier API:
rememberReorderStatecreates aReorderState, a thin wrapper that exposes the underlyingDragAndDropStateasreorderState.dndState(needed for modifiers likedragAutoScroll). It accepts the samedragAfterLongPress,requireFirstDownUnconsumed, andreorderHysteresisDistanceparameters.ReorderableItemaccepts the same parameters as thereorderableItemmodifier, butdraggableContentis optional -- whennull, the item content is used as the drag shadow.- The
contentlambda runs in aReorderableItemScopeprovidingkey,isDragging, and a scope-basedModifier.dragHandle()that doesn't needkey/statearguments.