Compose DND

repository·main·Indexed 19 days ago

https://github.com/mohamedrejeb/compose-dnd

A library for adding drag and drop functionality to Jetpack Compose and Compose Multiplatform projects, supporting Android, iOS, Desktop, and Web. It provides tools for implementing draggable items, drop targets, and reorderable lists using DragAndDropState, DragAndDropContainer, and specialized modifiers like reorderableItem and dragAutoScroll.

Tokens
22.9K
Snippets
51
Records
62
Agent score
66%

What's inside compose-dnd

  1. Overview of Compose DND Features

    main

    Compose DND provides a comprehensive set of features for drag and drop interactions:

    • Drag and Drop: Move items between locations and onto targets.
    • Reorder Lists: Rearrange items within a list.
    • Auto Scroll: Automatic scrolling when dragging near container edges.
    • Drop Strategies: Built-in logic to determine which target receives an item.
    • Drag Handle: Restrict dragging to a specific part of an item.
    • Axis Lock: Constrain movement to horizontal or vertical axes.
    • Conditional Drop: Filter which targets accept specific items.
    • Drop Animation: Spring-based animations for smooth drops.
    • Custom Drag Shadow: Use a custom Composable as the visual representation during dragging.
    • Enable/Disable: Toggle functionality at the container or individual item level.
  2. Handle overlapping targets with Z-Index

    main

    When drop targets overlap visually, the one with a higher zIndex takes priority in the drop strategy calculation. You can set the priority using the zIndex parameter in the dropTarget modifier.

    Box(
        modifier = Modifier
            .dropTarget(
                key = "background-target",
                state = dragAndDropState,
                zIndex = 0f, // Lower priority
                onDrop = { /* ... */ },
            )
    ) {
        Box(
            modifier = Modifier
                .dropTarget(
                    key = "foreground-target",
                    state = dragAndDropState,
                    zIndex = 1f, // Higher priority
                    onDrop = { /* ... */ },
                )
        )
    }
  3. Reorder columns using a second DragAndDropState

    main

    To allow users to reorder the columns themselves while cards are also draggable, you must implement a layered approach using two distinct DragAndDropState instances:

    1. Outer Layer: A DragAndDropContainer using a DragAndDropState<Column> to manage column movement.
    2. Inner Layer: A DragAndDropContainer using a DragAndDropState<Card> to manage card movement.

    Column Configuration Requirements:

    • hasDragHandle = true: This ensures only specific parts of the column (like a header) initiate a drag, preventing card gestures from accidentally moving the entire column.
    • dragAxis = DragAxis.Horizontal: Keeps the column aligned with the horizontal LazyRow during the drag.
    • draggableContent: Use a simplified preview (e.g., just the header) to keep the drag shadow performance-efficient.
    // Layered containers for dual-level dragging
    DragAndDropContainer(state = columnDndState) {
        DragAndDropContainer(state = cardDndState) {
            LazyRow(
                modifier = Modifier
                    .dragAutoScroll(state = cardDndState, lazyListState = rowState)
                    .dragAutoScroll(state = columnDndState, lazyListState = rowState),
            ) {
                // columns...
            }
        }
    }
    
    // Column implementation
    Column(
        modifier = Modifier
            .reorderableItem(
                key = "col-${column.id}",
                data = column,
                state = columnDndState,
                hasDragHandle = true,
                dragAxis = DragAxis.Horizontal,
                dropStrategy = DropStrategy.CenterDistance,
                onDragEnter = { state -> onColumnEnter(state.data, column) },
                draggableContent = { ColumnDragPreview(column) },
            )
    )
  4. Core concepts of Compose DND

    main

    Compose DND uses a declarative API built around four main components to implement drag and drop in Compose UI:

    1. DragAndDropState: The central state holder for all drag and drop operations. It tracks which items are being dragged and which targets are being hovered.
    2. DragAndDropContainer: A mandatory wrapper composable that manages pointer input and renders the drag shadow. All draggable items and drop targets must reside within this container.
    3. draggableItem modifier: Applied to composables to make them capable of being dragged. It requires a unique key, the data to be transferred, and a definition for the drag shadow content.
    4. dropTarget modifier: Applied to composables to designate them as valid destinations for dragged items.

    To implement a basic flow, you create a state, wrap your UI in a container, mark items as draggable, and mark targets as drop targets.

    val dragAndDropState = rememberDragAndDropState<String>()
    
    DragAndDropContainer(state = dragAndDropState) {
        // Draggable items and drop targets go here
    }
  5. Observe drag and hover state

    main

    You can query the DragAndDropState to react to the current drag lifecycle:

    1. Check if a specific target is hovered: Compare dragAndDropState.hoveredDropTargetKey with your target's key.
    2. Check if any item is being dragged: Check if dragAndDropState.draggedItem != null.
    3. Check if a specific item is being dragged: Use dragAndDropState.isDragging(key).
    // Check hover state for styling
    val isHovered = dragAndDropState.hoveredDropTargetKey == "target-1"
    
    // Check if any drag is active
    val isAnyItemDragging = dragAndDropState.draggedItem != null
  6. Implement reorder logic using onDragEnter

    main

    Reordering is achieved by updating your underlying data source inside the onDragEnter callback. When a dragged item enters the area of a target item, use the DraggedItemState provided to the callback to identify the dragged data and move it to the target's index.

    onDragEnter = { state ->
        items = items.toMutableList().apply {
            val targetIndex = indexOf(item)
            if (targetIndex != -1) {
                remove(state.data)
                add(targetIndex, state.data)
            }
        }
    }
  7. How Drag and Drop works in Compose DND

    main

    To implement drag and drop between different locations, follow these steps:

    1. Create State: Initialize DragAndDropState using rememberDragAndDropState<T>(), where T is the type of data being dragged.
    2. Wrap Content: Wrap your UI hierarchy in a DragAndDropContainer passing the created state.
    3. Define Draggable Items: Use the draggableItem modifier on the item you want to move. You must provide a unique key, the data to be transferred, and a draggableContent lambda which defines the composable to be shown as the drag shadow.
    4. Define Drop Targets: Use the dropTarget modifier on the destination composable. Provide a key and an onDrop callback that receives the state containing the dropped data.

    You can use dragAndDropState.isDragging(key) to check if a specific item is currently being dragged (e.g., to change its opacity).

    val dragAndDropState = rememberDragAndDropState<String>()
    
    DragAndDropContainer(
        state = dragAndDropState,
    ) {
        val isDragging = dragAndDropState.isDragging("item-1")
    
        Text(
            text = "Drag me",
            modifier = Modifier
                .graphicsLayer { alpha = if (isDragging) 0f else 1f }
                .draggableItem(
                    key = "item-1",
                    data = "Hello",
                    state = dragAndDropState,
                    draggableContent = {
                        Text("Drag me") // Shown as the drag shadow
                    },
                ),
        )
    
        Box(
            modifier = Modifier
                .dropTarget(
                    key = "target-1",
                    state = dragAndDropState,
                    onDrop = {
                        println("Dropped: ${it.data}")
                    },
                )
        ) {
            Text("Drop here")
        }
    }
  8. Prevent scroll jumps with dragScrollPin

    main

    When reordering items of different sizes, Compose's scroll anchoring can cause visible jumps in the viewport. The dragScrollPin modifier fixes this by pinning the scroll position right before each reorder swap, ensuring a stable viewport.

    Important: dragAutoScroll already includes scroll pinning behavior. Use dragScrollPin on its own only if you want jump-free reordering without automatic edge scrolling.

    @OptIn(ExperimentalDndApi::class)
    @Composable
    fun ScrollPinExample() {
        val dndState = rememberDragAndDropState<String>()
        val lazyListState = rememberLazyListState()
    
        DragAndDropContainer(
            state = dndState,
        ) {
            LazyColumn(
                state = lazyListState,
                modifier = Modifier
                    .fillMaxSize()
                    .dragScrollPin(
                        state = dndState,
                        lazyListState = lazyListState,
                    ),
            ) {
                // items with varying heights...
            }
        }
    }
  9. Compare dropTargets and canDrop parameters

    main

    The library provides two different ways to filter drag-and-drop interactions. They can be used together: the draggableItem first checks its dropTargets allowlist, and then the dropTarget checks its canDrop logic.

    FeaturedropTargets (on draggableItem)canDrop (on dropTarget)
    Defined onThe dragged itemThe drop target
    Filters byDrop target keys (allowlist)Custom logic based on DraggedItemState
    Use case"This item can only go to zones A, B""This zone only accepts certain items"
  10. Use the ReorderContainer wrapper API

    main

    If you prefer a wrapper-based approach over modifiers, use ReorderContainer and ReorderableItem. This API uses rememberReorderState<T>() which provides a ReorderState object.

    Key Differences:

    • ReorderState exposes the underlying DragAndDropState via reorderState.dndState.
    • ReorderableItem's content lambda runs in a ReorderableItemScope, providing access to key, isDragging, and a scope-specific Modifier.dragHandle().
    • draggableContent is optional in ReorderableItem; if null, the item's own content is used as the drag shadow.
    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
                            }
                    )
                }
            }
        }
    }
  11. Implement a Kanban Board with Multiple Lists

    main

    To build a Kanban board, you need to coordinate multiple lists (columns) and items (cards) that can move both within a list and between lists.

    Core Architecture

    • Shared State: Use a single DragAndDropState<Card> for all cards across all columns. This allows onDragEnter to fire when a card is dragged from one column into another.
    • Nested Containers: Wrap the entire board in a DragAndDropContainer. Use a LazyRow for columns and a LazyColumn for the cards within each column.
    • Auto-Scrolling: Apply .dragAutoScroll() to both the LazyRow (for horizontal column scrolling) and the LazyColumn (for vertical card scrolling) to ensure smooth movement at the edges.
    • Reordering: Use Modifier.reorderableItem on cards. Because they share the same DragAndDropState, the onDragEnter callback handles both same-column reordering and cross-column transfers.
    @OptIn(ExperimentalDndApi::class)
    @Composable
    fun KanbanBoard() {
        val dndState = rememberDragAndDropState<Card>()
        var columns by remember { mutableStateOf(initialColumns()) }
        val rowState = rememberLazyListState()
    
        DragAndDropContainer(state = dndState) {
            LazyRow(
                state = rowState,
                modifier = Modifier.dragAutoScroll(state = dndState, lazyListState = rowState),
            ) {
                items(columns, key = { it.id }) {
                    // Column UI implementation...
                }
            }
        }
    }