Reorderable

repository·main·Indexed 23 days ago

https://github.com/calvin-ll/reorderable

A Jetpack Compose and Compose Multiplatform library that adds drag-and-drop reordering capabilities to standard and lazy layout components, including LazyColumn, LazyRow, LazyVerticalGrid, LazyHorizontalGrid, LazyVerticalStaggeredGrid, LazyHorizontalStaggeredGrid, Column, and Row. It supports Android, iOS, Desktop/JVM, Wasm, and JS, featuring customizable drag handles, support for items of different sizes, and automatic scrolling when dragging near screen edges.

Tokens
13.6K
Snippets
29
Records
43
Agent score
30%

What's inside Reorderable

  1. Overview of Reorderable

    main
    Reorderable is a library for Jetpack Compose and Compose Multiplatform that enables drag-and-drop reordering of items within various layout components. It provides support for both standard layouts and lazy-loading lists/grids, allowing users to intuitively rearrange content via touch gestures.
  2. Key features of Reorderable

    main

    Reorderable is a Compose Multiplatform library designed for handling item reordering in lazy layouts. Key capabilities include:

    • Multiplatform Support: Works with Android, iOS, Desktop/JVM, Wasm, and JS.
    • Flexible Layouts: Supports items of different sizes, section headers, and footers.
    • Customizable Reordering:
      • Some items can be marked as non-reorderable.
      • Supports using a specific child of an item as a drag handle.
      • Supports both immediate dragging and long-press to start dragging.
    • Advanced Animation & Scrolling:
      • Uses Modifier.animateItem for smooth movement within lazy components.
      • Supports dragging and animating the first visible item.
      • Automatic scrolling when dragging near the edge of the screen (note: scrolling is unavailable when using standard Column or Row instead of lazy components; scroll speed scales with distance from the edge).
    • Compatible Lazy Components: Works with LazyColumn, LazyRow, LazyVerticalGrid, LazyHorizontalGrid, LazyVerticalStaggeredGrid, and LazyHorizontalStaggeredGrid.
  3. Handle section headers and footers in reordering

    main

    When using LazyVerticalStaggeredGrid with non-item elements like section headers or footers, the from.index and to.index provided in the onMove lambda refer to the absolute indices within the grid. You must adjust these indices to account for the offset caused by headers/footers when updating your underlying data list.

    Example: If you have one header at index 0, you must subtract 1 from the indices to access the correct item in your list.

    var list by remember { mutableStateOf(List(100) { "Item $it" }) }
    val lazyStaggeredGridState = rememberLazyStaggeredGridState()
    val reorderableLazyStaggeredGridState = rememberReorderableLazyStaggeredGridState(lazyStaggeredGridState) { from, to ->
        list = list.toMutableList().apply {
            // Subtract 1 to account for the header at index 0
            add(to.index - 1, removeAt(from.index - 1))
        }
    }
    
    LazyVerticalStaggeredGrid(
        state = lazyStaggeredGridState,
        // ...
    ) {
        item {
            Text("Header")
        }
    
        items(list, key = { item -> item.id }) {
            ReorderableItem(reorderableLazyStaggeredGridState, item.id) {
                // ...
            }
        }
    }
  4. Handle section headers and footers in reorderable lists

    main

    When your LazyRow or LazyColumn contains non-reorderable items like section headers or footers, the from.index and to.index provided in the onMove callback will reflect the absolute indices in the list. You must manually adjust these indices to map to your actual data list to avoid reordering the headers or causing index out of bounds errors.

    var list by remember { mutableStateOf(List(100) { "Item $it" }) }
    val lazyListState = rememberLazyListState()
    val reorderableLazyRowState = rememberReorderableLazyListState(lazyListState) { from, to ->
        // Adjusting indices because the first item in the LazyRow is a header
        list = list.toMutableList().apply {
            add(to.index - 1, removeAt(from.index - 1))
        }
    }
    
    LazyRow(
        state = lazyListState,
        // ...
    ) {
        item {
            Text("Header")
        }
    
        items(list, key = { item -> item.id }) { item ->
            ReorderableItem(reorderableLazyRowState, item.id) {
                // ...
            }
        }
    }
  5. Supported Compose Layouts for Reordering

    main

    Reorderable supports the following Jetpack Compose and Compose Multiplatform components:

    Lazy Components:

    • LazyColumn
    • LazyRow
    • LazyVerticalGrid
    • LazyHorizontalGrid
    • LazyVerticalStaggeredGrid
    • LazyHorizontalStaggeredGrid

    Standard Layouts:

    • Column
    • Row
  6. Adjust indices for Section Headers and Footers

    main

    The from.index and to.index provided in the onMove callback of rememberReorderableLazyStaggeredGridState correspond to the indices of the items within the LazyHorizontalStaggeredGrid. If your list contains non-reorderable items like section headers or footers, you must manually adjust the indices in your list update logic to account for their presence.

    var list by remember { mutableStateOf(List(100) { "Item $it" }) }
    val lazyStaggeredGridState = rememberLazyStaggeredGridState()
    val reorderableLazyStaggeredGridState = rememberReorderableLazyStaggeredGridState(lazyStaggeredGridState) { from, to ->
        list = list.toMutableList().apply {
            // Adjusting for a header at index 0
            add(to.index - 1, removeAt(from.index - 1))
        }
    }
    
    LazyHorizontalStaggeredGrid(
        state = lazyStaggeredGridState,
        // ...
    ) {
        item {
            Text("Header")
        }
    
        items(list, key = { item -> item.id }) { item ->
            ReorderableItem(reorderableLazyStaggeredGridState, item.id) {
                // ...
            }
        }
    }
  7. Handle Section Headers and Footers in LazyColumn

    main

    When using LazyColumn with non-item elements like section headers or footers, the from.index and to.index provided in the onMove lambda refer to the absolute indices in the LazyColumn. You must adjust these indices to match your data list indices. For example, if a header is at index 0, your list items start at index 1, so you may need to subtract 1 from the indices.

    var list by remember { mutableStateOf(List(100) { "Item $it" }) }
    val lazyListState = rememberLazyListState()
    val reorderableLazyColumnState = rememberReorderableLazyListState(lazyListState) { from, to ->
        // Adjusting indices for a header at index 0
        list = list.toMutableList().apply {
            add(to.index - 1, removeAt(from.index - 1))
        }
    }
    
    LazyColumn(
        state = lazyListState,
        // ...
    ) {
        item {
            Text("Header")
        }
    
        items(list, key = { item -> item.id }) { item ->
            ReorderableItem(reorderableLazyColumnState, item.id) {
                // ...
            }
        }
    }
  8. Use Reorderable with Material3 Clickable Cards

    main

    To use Material3's Card with an onClick handler alongside reordering, you must share a MutableInteractionSource between the Card and the drag handle (Modifier.draggableHandle or Modifier.longPressDraggableHandle). This allows the Card to correctly respond to drag events emitted by the handle.

    Steps:

    1. Create a MutableInteractionSource using remember { MutableInteractionSource() }.
    2. Pass this instance to the Card's interactionSource parameter.
    3. Pass the same instance to the Modifier.draggableHandle's interactionSource parameter.
    val interactionSource = remember { MutableInteractionSource() }
    Card(
        onClick = {},
        interactionSource = interactionSource,
    ) {
        Row {
            Text(item, Modifier.padding(horizontal = 8.dp))
            IconButton(
                modifier = Modifier.draggableHandle(
                    onDragStarted = {
                        hapticFeedback.performHapticFeedback(HapticFeedbackType.GestureThresholdActivate)
                    },
                    onDragStopped = {
                        hapticFeedback.performHapticFeedback(HapticFeedbackType.GestureEnd)
                    },
                    interactionSource = interactionSource,
                ),
                onClick = {},
            ) {
                Icon(Icons.Rounded.DragHandle, contentDescription = "Reorder")
            }
        }
    }
  9. Improve accessibility for reorderable lists

    main

    When making reorderable lists accessible for TalkBack users:

    1. If items only contain a drag handle, add custom actions like "Move Up", "Move Down", "Move Left", or "Move Right" using SemanticsPropertyReceiver.customActions.
    2. Apply Modifier.clearAndSetSemantics to the drag handle button to make it non-focusable for TalkBack, preventing redundant focus points.
  10. Install Reorderable using Version Catalog

    main

    To use Reorderable with Gradle Version Catalogs, add the version and library definition to your libs.versions.toml file, then implement it in your dependencies block.

    [versions]
    #...
    reorderable = "3.1.0"
    
    [libraries]
    #...
    reorderable = { module = "sh.calvin.reorderable:reorderable", version.ref = "reorderable" }
    dependencies {
        // ...
        implementation(libs.reorderable)
    }
  11. Use Reorderable with LazyColumn

    main

    To implement reordering in a LazyColumn, follow these steps:

    1. Create a lazyListState using rememberLazyListState().
    2. Create a reorderableLazyListState using rememberReorderableLazyListState(lazyListState) { from, to -> ... }. The lambda is where you update your underlying data source.
    3. Pass the lazyListState to the LazyColumn.
    4. Wrap your items in ReorderableItem(reorderableLazyListState, key = /* item key */).
    5. Use Modifier.draggableHandle() on a component within the ReorderableItem to act as the drag trigger.
    val lazyListState = rememberLazyListState()
    val reorderableLazyListState = rememberReorderableLazyListState(lazyListState) { from, to ->
        // Update the list
    }
    
    LazyColumn(state = lazyListState) {
        items(list, key = { /* item key */ }) {
            ReorderableItem(reorderableLazyListState, key = /* item key */) { isDragging ->
                // Item content
                IconButton(
                    modifier = Modifier.draggableHandle(),
                    /* ... */
                )
            }
        }
    }
  12. Implement reordering in LazyVerticalGrid

    main

    Reordering in a LazyVerticalGrid follows a similar pattern to LazyRow. Use rememberReorderableLazyGridState (passing the LazyGridState) and wrap items in ReorderableItem.

    Note: The onMove callback provides indices that correspond to the grid positions.

    val lazyGridState = rememberLazyGridState()
    val reorderableLazyGridState = rememberReorderableLazyGridState(lazyGridState) { from, to ->
        // Update the list
    }
    
    LazyVerticalGrid(state = lazyGridState) {
        items(list, key = { /* item key */ }) {
            ReorderableItem(reorderableLazyGridState, key = /* item key */) { isDragging ->
                // Item content
                IconButton(
                    modifier = Modifier.draggableHandle(),
                    /* ... */
                )
            }
        }
    }