reorderable_wrap

Drag-to-reorder layouts that Flutter does not ship: a multi-line Wrap, a lazy grid, a Table, and non-scrolling Row/Column. Maintained, dependency-free, and consistent across all widgets.

pub package License: MIT

Features

  • ReorderableWrap: reorder children of a Wrap, with multi-line flow, spacing, runSpacing, every alignment option, and children of different sizes.
  • ReorderableGridView.builder and SliverReorderableGrid: a lazily built grid from an itemBuilder, for large collections.
  • ReorderableTable: reorder rows while cells stay aligned to the columns, with an optional fixed header row.
  • ReorderableRow and ReorderableColumn: non-scrolling, for small sets. Expanded and Flexible children keep flexing.
  • One index convention everywhere: newIndex is the index after the move.
  • onReorder, onReorderStart, onReorderEnd, onNoReorder and buildDraggableFeedback.
  • Drag start by long press (default), immediately, or only from a ReorderableHandle.
  • Animated displacement of the other items, and an animated drop.
  • Auto-scroll near the edges of any enclosing Scrollable, including nested scroll views: the inner view scrolls first, then the outer one.
  • Optional haptic feedback, per-item enabled flag, RTL, and a whole-widget enabled switch.
  • Accessibility: every item gets the "Move to the start", "Move up/left", "Move down/right" and "Move to the end" custom semantics actions, the same pattern (and localized labels) as ReorderableListView.
  • Children keep their State while dragged and after they move. Keys are required and checked: a missing or duplicate key fails with a clear error.

Install

dependencies:
  reorderable_wrap: ^0.1.0

Usage

The index convention

onReorder(oldIndex, newIndex) gives the index the item will have after the move. Applying it is always one line, for every widget in this package:

items.insert(newIndex, items.removeAt(oldIndex));

Wrap

ReorderableWrap(
  spacing: 8,
  runSpacing: 8,
  onReorder: (oldIndex, newIndex) => setState(
    () => tags.insert(newIndex, tags.removeAt(oldIndex)),
  ),
  children: [
    for (final tag in tags) Chip(key: ValueKey(tag), label: Text(tag)),
  ],
)

Grid

ReorderableGridView.builder(
  gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
    crossAxisCount: 4,
  ),
  itemCount: photos.length,
  itemBuilder: (context, index) => Card(
    key: ValueKey(photos[index].id),
    child: Image.network(photos[index].url, fit: BoxFit.cover),
  ),
  onReorder: (oldIndex, newIndex) => setState(
    () => photos.insert(newIndex, photos.removeAt(oldIndex)),
  ),
)

Inside a CustomScrollView, use SliverReorderableGrid with the same arguments.

Table

ReorderableTable(
  header: const [Text('Item'), Text('Qty')],
  columnWidths: const {0: FlexColumnWidth(2), 1: FlexColumnWidth()},
  onReorder: (oldIndex, newIndex) => setState(
    () => rows.insert(newIndex, rows.removeAt(oldIndex)),
  ),
  children: [
    for (final row in rows)
      ReorderableTableRow(
        key: ValueKey(row.id),
        children: [Text(row.name), Text('${row.qty}')],
      ),
  ],
)

Row and Column

ReorderableColumn(
  onReorder: (oldIndex, newIndex) => setState(
    () => steps.insert(newIndex, steps.removeAt(oldIndex)),
  ),
  children: [
    for (final step in steps)
      ListTile(key: ValueKey(step), title: Text(step)),
  ],
)

Drag handles, disabled items, feedback

ReorderableWrap(
  dragStart: ReorderDragStart.handleOnly,
  buildDraggableFeedback: (context, constraints, child) => Transform.scale(
    scale: 1.05,
    child: Material(elevation: 6, child: child),
  ),
  onReorder: onReorder,
  children: [
    for (final card in cards)
      ReorderableItem(
        key: ValueKey(card.id),
        enabled: !card.locked,
        child: Row(mainAxisSize: MainAxisSize.min, children: [
          const ReorderableHandle(child: Icon(Icons.drag_indicator)),
          Text(card.title),
        ]),
      ),
  ],
)

Shared options

Every widget accepts:

Option Default Meaning
onReorder required (oldIndex, newIndex), newIndex after removal
onReorderStart null drag started at index
onReorderEnd null item released; index it will have after the move
onNoReorder null item dropped where it started
buildDraggableFeedback shadow decorates the item that follows the pointer
dragStart longPress longPress, immediate or handleOnly
enabled true turns reordering off for the whole widget
hapticFeedback true selection click when a drag starts
animationDuration 200 ms displacement and drop animations; Duration.zero disables them
animationCurve easeInOut curve of those animations
autoScroll true scroll enclosing scrollables near their edges
autoScrollEdge 48 edge zone size in logical pixels
autoScrollSpeed 1200 maximum speed in logical pixels per second
placeholderOpacity 0 opacity of the item left at the drop position

When to use Flutter's ReorderableListView instead

Flutter already ships ReorderableListView and SliverReorderableList. For a single-column, scrolling list (a to-do list, a playlist), use them: they are lazy, part of the framework, and well tested. This package does not duplicate them.

Use this package when you need a layout they do not cover: items that flow into several lines (ReorderableWrap), a grid (ReorderableGridView, SliverReorderableGrid), table rows with aligned columns (ReorderableTable), or a short non-scrolling row or column placed inside other content (ReorderableRow, ReorderableColumn).

Note that ReorderableListView counts newIndex before removal, so moving an item down needs if (newIndex > oldIndex) newIndex -= 1;. This package counts it after removal and needs no adjustment.

Migrating from reorderables

reorderables 0.6.0 was last released in January 2023 and its SDK constraint excludes Dart 3. This package is a clean-room rewrite, not a fork, with close but not identical names:

reorderables reorderable_wrap
import 'package:reorderables/reorderables.dart' import 'package:reorderable_wrap/reorderable_wrap.dart'
ReorderableWrap, ReorderableRow, ReorderableColumn same names
ReorderableTable(children: [ReorderableTableRow(children: ...)]) same shape; ReorderableTableRow now requires key and is a plain row description, not a widget
ReorderableSliverList use Flutter's SliverReorderableList
(no grid) ReorderableGridView.builder, SliverReorderableGrid
onReorderStarted onReorderStart
onNoReorder onNoReorder
buildDraggableFeedback(context, constraints, child) same signature
needsLongPressDraggable: false dragStart: ReorderDragStart.immediate
ReorderableWidget(reorderable: false, key: ..., child: ...) ReorderableItem(enabled: false, key: ..., child: ...)
enableReorder: false enabled: false
reorderAnimationDuration animationDuration
draggingWidgetOpacity placeholderOpacity
scrollController, controller, scrollDirection, scrollPhysics, ignorePrimaryScrollController removed: the widgets never create a scroll view. Wrap them in your own scroll view; auto-scroll finds it
header, footer (wrap, flex) removed: put them next to the widget, for example in a Column
minMainAxisCount, maxMainAxisCount removed
buildItemsContainer removed

Behaviour differences to check when migrating:

  • Index semantics are the same (newIndex after removal), so an existing insert(newIndex, removeAt(oldIndex)) handler keeps working.
  • Every child must have a unique key. reorderables sometimes worked without keys; this package fails fast with an error naming the index.
  • Children keep their State during a drag (reorderables rebuilt them, losing text field focus and animations).
  • Auto-scroll uses the enclosing scroll views instead of an internal controller, which removes the "ScrollController attached to multiple scroll views" errors and works in CustomScrollView and nested views.
  • The drop target is computed from where the item would actually be laid out, so items of different sizes, index 0 and the last position all work.

Limitations

  • Items cannot be dragged between two different reorderable widgets, and several items cannot be moved at once.
  • The drag feedback is a second copy of the child built in the Overlay. If a child carries a GlobalKey, provide buildDraggableFeedback that does not reuse child, or the key would be duplicated.
  • SliverReorderableGrid assumes cells of equal size (the standard SliverGridDelegateWithFixedCrossAxisCount and SliverGridDelegateWithMaxCrossAxisExtent). Staggered or quilted layouts are not supported.
  • ReorderableWrap, ReorderableTable, ReorderableRow and ReorderableColumn build all children eagerly, like the Flutter widgets they wrap. Use the grid, or Flutter's ReorderableListView, for long lists.
  • In a lazy grid, a missing or duplicate key is reported when the offending item is built, not up front.
  • If the dragged child is removed from the list during a drag, the drag is cancelled: onReorderEnd fires, onReorder and onNoReorder do not.
  • If your onReorder updates the list asynchronously (for example through a stream), the items briefly return to their old order until the new list arrives.

License

MIT, see LICENSE.

Made by Abdeldjalil Chougui.

Libraries

reorderable_wrap
Drag-to-reorder Wrap, lazy grid, table, Row and Column.