In Flutter, how do you make a list the user can drag into a new order with ReorderableListView, and what must onReorderItem do?
answer
- every row needs a key
- old index and new index
- remove, then insert, in setState
- newIndex already adjusted since 3.44
- onReorder is the deprecated one
basics
~20 sBuild the rows with ReorderableListView or its builder constructor, give every row a key, and in onReorderItem move the item in your data with removeAt(oldIndex) then insert(newIndex) inside setState; since Flutter 3.44 newIndex already accounts for the removal.
solid answer
~40 s`ReorderableListView` (or `ReorderableListView.builder`) handles the drag, the gap animation and auto-scrolling; my job is the data. Every row must carry a key, otherwise it fails with 'Every item of ReorderableListView must have a key.'. When a row is dropped somewhere new, `onReorderItem(oldIndex, newIndex)` fires, and I update the list inside `setState`: `final item = items.removeAt(oldIndex); items.insert(newIndex, item);`. In Flutter 3.44 and later `newIndex` is already the index after the removal, so no correction is needed. The older `onReorder` callback, now deprecated, reported the index before removal and every handler had to do `if (oldIndex < newIndex) newIndex -= 1;`. Passing both callbacks trips an assertion, and keeping that correction after migrating puts rows one slot too high. Dropping a row back in place does not call `onReorderItem`; `onReorderEnd` still fires.
code
dart · 39 linesimport 'package:flutter/material.dart';
class PackingItem {
const PackingItem({required this.id, required this.label});
final String id;
final String label;
}
class PackingList extends StatefulWidget {
const PackingList({super.key, required this.initialItems});
final List<PackingItem> initialItems;
@override
State<PackingList> createState() => _PackingListState();
}
class _PackingListState extends State<PackingList> {
late final List<PackingItem> _items = [...widget.initialItems];
void _move(int oldIndex, int newIndex) {
setState(() {
// onReorderItem reports newIndex after the removal: no correction.
final item = _items.removeAt(oldIndex);
_items.insert(newIndex, item);
});
}
@override
Widget build(BuildContext context) {
return ReorderableListView.builder(
itemCount: _items.length,
onReorderItem: _move,
itemBuilder: (context, index) {
final item = _items[index];
return ListTile(key: ValueKey(item.id), title: Text(item.label));
},
);
}
}go deeper
Recall the three parts: ReorderableListView or its builder, a key on every row, and onReorderItem that removes the item at oldIndex and inserts it at newIndex inside setState.
Explain what changed in 3.44: onReorder reported newIndex before removal and needed a minus-one fix, while onReorderItem is already corrected and is skipped when nothing moved.
Show migration judgement: find handlers that kept the old correction, move persistence to the state layer or onReorderEnd, and keep the data stable during a drag.
Decide where ordering lives, locally or synced, and how a reorder on one device merges with edits from another before offering drag-to-reorder at all.
## What ReorderableListView does `ReorderableListView` is the Material widget for a **drag-to-reorder list**: the user picks a row up, the other rows open a gap as it moves, the list auto-scrolls near its edges, and on drop the widget tells the app the old and new positions. It does **not** reorder anything itself. The data belongs to the app, and the widget only reports what the user asked for. It comes in two shapes: - `ReorderableListView(children: [...])` for a short, fixed list of rows; - `ReorderableListView.builder(itemCount:, itemBuilder:)` for a lazy list built on demand. Both take the same callbacks. The widgets layer has the same machinery without Material styling as `ReorderableList` and `SliverReorderableList`. ## Keys are mandatory Each row must carry a key; the widget checks it and fails with 'All children of this widget must have a key.' (children constructor) or 'Every item of ReorderableListView must have a key.' (builder). A stable key taken from the data, such as `ValueKey(item.id)`, is the usual choice. Why keys preserve identity when rows move is a topic of its own; here it is simply a requirement. ## The callbacks | Callback | When it runs | Typical use | |---|---|---| | `onReorderItem(oldIndex, newIndex)` | on drop, when the index changed | move the item in your data | | `onReorderStart(index)` | when a drag begins | haptics, hide other controls | | `onReorderEnd(index)` | on every drop, even in place | restore controls, persist order | | `onReorder(oldIndex, newIndex)` | deprecated since 3.44 | migrate to `onReorderItem` | Exactly one of `onReorderItem` and `onReorder` must be given; passing both fails with 'The onReorder callback is obsolete and is replaced by onReorderItem.'. ## The newIndex change in 3.44 The old `onReorder` reported `newIndex` as a position in the list **before** the dragged item was removed. Moving an item down therefore needed a correction, because removing it first shortens the list by one: 1. The list is `[passport, charger, adapter, socks]`. 2. The user drags `passport` (index 0) to just below `adapter`. 3. `onReorder` reported `newIndex == 3`; the handler had to subtract one before inserting. 4. `onReorderItem` reports `newIndex == 2`, ready to use after `removeAt(0)`. The framework now does the subtraction itself: when `newIndex > oldIndex` it decrements before calling `onReorderItem`, and it skips the call entirely when the two are equal. The migration is not applied by `dart fix`, because the parameter's meaning changed rather than its name. The classic migration bug is to rename the callback and keep the old `if (oldIndex < newIndex) newIndex -= 1;`; downward moves then land one row above the drop point. ## Updating the data The handler should be small and synchronous: - remove the item at `oldIndex` and insert it at `newIndex`, inside `setState` or through whatever state object owns the list; - persist the new order afterwards if needed, for example in `onReorderEnd` or from the state layer; - do not rebuild the row list from a different source during the drag, or the dragged row's index goes stale. ## A packing list in a travel app A trip's packing list is the natural fit: the traveller drags 'passport' to the top and 'socks' further down. Each `PackingItem` has an `id`, the rows use `ValueKey(item.id)`, and `onReorderItem` moves the item in a `List<PackingItem>`. Because the list is short, either constructor works; the builder is the habit worth keeping for longer lists. ## Checklist for a reorderable list 1. Choose the constructor: `children` for a handful of fixed rows, `.builder` for anything that grows. 2. Give every row a stable key derived from the data, never from the index, since the index is exactly what changes. 3. Implement `onReorderItem` as a plain remove-then-insert on the data, with no index arithmetic. 4. Put side effects that belong to the gesture, such as haptics or saving, in `onReorderStart` and `onReorderEnd` or in the state layer. 5. When upgrading an older codebase, search for `onReorder:` and for the `newIndex -= 1` line, and remove the line in the same change as the rename. 6. Test a move up and a move down: an off-by-one only shows in one direction.
- What does the old onReorder handler look like, and why can dart fix not migrate it?It reported `newIndex` before removal, so handlers began with `if (oldIndex < newIndex) newIndex -= 1;`. `onReorderItem` takes the same two ints but with the corrected meaning. A mechanical rename would keep the correction and shift every downward move by one, so the migration needs a person to delete that line, or to add one back to reproduce the old value.
- Where would you save the new order to storage?Not inside the drag. Update the in-memory list in `onReorderItem`, then persist from the state layer or in `onReorderEnd`, which fires on every drop even when nothing moved. Saving asynchronously and then rebuilding the list from storage mid-drag risks stale indices.
saying these in an interview costs you the question
- ReorderableListView reorders the children list for you, so no handler is needed.
- Keep the if (oldIndex < newIndex) newIndex -= 1 correction after moving to onReorderItem.
- Rows can omit keys because the list tracks them by index.
- Pass both onReorder and onReorderItem during a migration to be safe.
- onReorderItem runs on every drop, even when the row returns to its place.