reorderable_wrap 0.1.0
reorderable_wrap: ^0.1.0 copied to clipboard
Drag-to-reorder Wrap, lazy grid, table, Row and Column with animated displacement, auto-scroll, drag handles, RTL and accessibility actions.
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.