Skip to content

Repository files navigation

SlotGrid

A drag-to-reorder grid for Compose Multiplatform whose items span multiple cells.

Maven Central License Platforms

Compose has LazyVerticalGrid for spans and a handful of libraries for drag-to-reorder lists. Nothing does both: a grid where a tile is two cells wide, another is two-by-two, and the user rearranges them by dragging.

▶ Try it in your browser — the sample compiled to WebAssembly.

implementation("io.github.nghicv:slotgrid-compose:0.1.1")

Usage

var items by remember { mutableStateOf(tiles) }

SlotGrid(
    items = items.map { SlotItem(it.id, it.span) { TileContent(it) } },
    onMove = { from, to -> items = items.toMutableList().apply { add(to, removeAt(from)) } },
    onSettle = { key, index -> viewModel.persistOrder(key, index) },
    columns = 4,
    scrollState = scrollState,
)

That is the whole API. The grid never inspects your content — it positions a box and calls you.

Two callbacks, not one

onMove fires continuously during a drag; apply it to your in-memory list so the preview reflows under the finger. onSettle fires once, on release, and only if the order actually changed; that is where you write to a database.

Collapsing them into a single callback forces a choice between a preview that lags and a database write on every frame.

Layout is derived, never stored

SlotGrid places items from order plus span. It does not store (x, y).

That is the decision everything else rests on. Absolute coordinates need their own conflict rule when two devices edit the same list, can leave holes nobody repairs, and turn resizing one item into a correction pass over every other one. Derived layout has none of those failure modes: every reachable state is a legal layout, and reordering is a list operation rather than a geometry problem.

The packing preserves monotonic reading order — item n never lands earlier than item n - 1. This costs density: a later small item is not pulled forward into an earlier gap, so grids are sometimes less tightly packed than they could be. That is deliberate. Without it, a user drags an item to second place and watches it appear somewhere in the middle.

The packer is public and has no UI dependency, if you want the layout without the grid:

val placements: List<SlotPlacement> = packSlots(spans, columns = 4)
val rows: Int = slotRowCount(placements)

Gestures

One continuous gesture per item, three phases, no modes:

The user Result
Taps onClick, with the item's window bounds so a transition can start from it
Moves before the hold completes Nothing is consumed — the scroll container underneath takes over
Holds The item lifts, with haptic feedback and onLongPress
Holds, then moves A drag begins; onDragStart fires so you can dismiss any menu

Pass a ScrollState and the grid auto-scrolls when a dragged item reaches the viewport edge.

Customising

Every visual constant is a parameter. SlotGridDefaults holds the defaults, and SlotLiftStyle controls how an item looks while it is held.

SlotGrid(
    // ...
    columns = 3,
    spacing = 8.dp,
    cellAspectRatio = 1f,
    liftStyle = SlotGridDefaults.liftStyle(scale = 1.06f, tiltDegrees = 0f),
)

Platforms

Android · iOS · JVM desktop · Wasm browser. Pure commonMain — the library contains no expect/actual at all.

Used in production

Boxlet — a visual journaling app on the App Store and Google Play — is where this component came from and where it runs today, on both platforms, from one Compose codebase.

Running the sample

./gradlew :sample:run                      # desktop
./gradlew :sample:wasmJsBrowserDevelopmentRun   # browser
./gradlew :sample:androidApp:installDebug  # Android device

License

Apache 2.0. See LICENSE.

About

Drag-to-reorder grid for Compose Multiplatform where items span multiple cells. Android, iOS, desktop, web.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages