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.
Features
ReorderableWrap: reorder children of aWrap, with multi-line flow,spacing,runSpacing, every alignment option, and children of different sizes.ReorderableGridView.builderandSliverReorderableGrid: a lazily built grid from anitemBuilder, for large collections.ReorderableTable: reorder rows while cells stay aligned to the columns, with an optional fixed header row.ReorderableRowandReorderableColumn: non-scrolling, for small sets.ExpandedandFlexiblechildren keep flexing.- One index convention everywhere:
newIndexis the index after the move. onReorder,onReorderStart,onReorderEnd,onNoReorderandbuildDraggableFeedback.- 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
enabledflag, RTL, and a whole-widgetenabledswitch. - 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
Statewhile 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 (
newIndexafter removal), so an existinginsert(newIndex, removeAt(oldIndex))handler keeps working. - Every child must have a unique key.
reorderablessometimes worked without keys; this package fails fast with an error naming the index. - Children keep their
Stateduring a drag (reorderablesrebuilt 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
CustomScrollViewand 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 aGlobalKey, providebuildDraggableFeedbackthat does not reusechild, or the key would be duplicated. SliverReorderableGridassumes cells of equal size (the standardSliverGridDelegateWithFixedCrossAxisCountandSliverGridDelegateWithMaxCrossAxisExtent). Staggered or quilted layouts are not supported.ReorderableWrap,ReorderableTable,ReorderableRowandReorderableColumnbuild all children eagerly, like the Flutter widgets they wrap. Use the grid, or Flutter'sReorderableListView, 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:
onReorderEndfires,onReorderandonNoReorderdo not. - If your
onReorderupdates 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.