Flutter ships a good widget for reordering: ReorderableListView. It does one thing, a single-column scrolling list, and does it well. The trouble is that many things users want to drag around are not single-column lists. Tags that flow over several lines. A grid of photos. Table rows whose cells must stay lined up with the columns. A short row of steps inside a form.

For those, the usual package was reorderables. Its last release, 0.6.0, dates from January 2023, and its SDK constraint excludes Dart 3.

reorderable_wrap provides the drag-to-reorder layouts that Flutter does not ship: a multi-line Wrap, a lazy grid, a Table, and non-scrolling Row and Column. It has no dependencies, and all the widgets behave the same way.

What it does

Five layouts.

  • ReorderableWrap reorders children of a Wrap, with multi-line flow, spacing, runSpacing, every alignment option, and children of different sizes.
  • ReorderableGridView.builder and SliverReorderableGrid give a lazily built grid from an itemBuilder, for large collections.
  • ReorderableTable reorders rows while cells stay aligned to the columns, with an optional fixed header row.
  • ReorderableRow and ReorderableColumn are non-scrolling, for small sets. Expanded and Flexible children keep flexing.

One index convention everywhere. newIndex is the index after the move. More on that below, because it removes a classic bug.

Drag control. A drag starts by long press (the default), immediately, or only from a ReorderableHandle. There are callbacks for onReorder, onReorderStart, onReorderEnd and onNoReorder, and buildDraggableFeedback to decorate the item that follows the pointer.

Motion. The other items are displaced with an animation, and the drop is animated too.

Auto-scroll. Dragging near an edge scrolls any enclosing Scrollable, including nested scroll views: the inner view scrolls first, then the outer one.

The details. Optional haptic feedback, a per-item enabled flag, a whole-widget enabled switch, and RTL.

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.

State is kept. 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.

How to use it

Add it from GitHub:

dependencies: reorderable_wrap: git: https://github.com/AbdeldjalilChougui/reorderable_wrap 

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 the package:

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

Compare that with ReorderableListView, which counts newIndex before removal, so moving an item down needs if (newIndex > oldIndex) newIndex -= 1;. Here no adjustment is needed.

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}')], ), ], ) 

Handles, locked items and 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), ]), ), ], ) 

Every widget accepts the same options. Besides the callbacks, the main ones are dragStart (longPress, immediate or handleOnly), hapticFeedback (on by default), animationDuration (200 ms; Duration.zero disables the animations), autoScroll with autoScrollEdge and autoScrollSpeed, and placeholderOpacity for the item left at the drop position.

When to use ReorderableListView instead

The README is clear that this package does not duplicate what Flutter already has. For a single-column, scrolling list such as a to-do list or a playlist, use ReorderableListView or SliverReorderableList: they are lazy, part of the framework, and well tested.

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

Migrating from reorderables

The package is a clean-room rewrite, not a fork, with close but not identical names. ReorderableWrap, ReorderableRow and ReorderableColumn keep their names, and ReorderableTable keeps its shape. The README has the full table; the main renames are:

  • onReorderStarted becomes onReorderStart.
  • needsLongPressDraggable: false becomes dragStart: ReorderDragStart.immediate.
  • ReorderableWidget(reorderable: false, ...) becomes ReorderableItem(enabled: false, ...).
  • reorderAnimationDuration becomes animationDuration, and draggingWidgetOpacity becomes placeholderOpacity.
  • The scroll parameters (scrollController, scrollDirection and the others) are removed. The widgets never create a scroll view. You wrap them in your own, and auto-scroll finds it.
  • header and footer are removed from the wrap and flex widgets. Put them next to the widget, for example in a Column.

The index semantics are the same as in reorderables, so an existing insert(newIndex, removeAt(oldIndex)) handler keeps working. Three behaviour differences are worth checking: every child must have a unique key, children keep their State during a drag (so a text field keeps its focus), and auto-scroll uses the enclosing scroll views instead of an internal controller.

Things to know

  • One widget at a time. Items cannot be dragged between two different reorderable widgets, and several items cannot be moved at once.
  • GlobalKey children. The drag feedback is a second copy of the child built in the Overlay. If a child carries a GlobalKey, provide a buildDraggableFeedback that does not reuse child, or the key would be duplicated.
  • Equal grid cells. SliverReorderableGrid assumes cells of equal size, as with the two standard grid delegates. Staggered or quilted layouts are not supported.
  • Eager building. 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.
  • Lazy grid keys. In a lazy grid, a missing or duplicate key is reported when the offending item is built, not up front.
  • Removal during a drag. If the dragged child is removed from the list during a drag, the drag is cancelled: onReorderEnd fires, onReorder and onNoReorder do not.
  • Async updates. 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.

Why I built it

Reordering is a small interaction with a lot of ways to feel wrong: the item jumps, the list does not scroll when you reach the edge, a text field loses focus, the index is off by one when moving down. Flutter solved it for the list. For wraps, grids and tables, the package that covered them stopped before Dart 3.

I wanted the missing layouts with one set of rules: the same options on every widget, the same index convention, keys checked up front, and the same accessibility actions that ReorderableListView offers, so a screen reader user can reorder a grid as well as a list.

Get the code

The source, the option table and the migration guide are on GitHub: https://github.com/AbdeldjalilChougui/reorderable_wrap. It's MIT licensed. Issues and pull requests are welcome.

Originally published at abdeldjalilchougui.biz.