moveItem method
Repositions itemKey, either across sections or within its own.
• toSection non-null → reparents the item under that section.
With index it lands at that position; without, it is
appended.
• toSection null, index non-null → reorders the item within
its current section to index.
• both null → no-op.
Both forms animate when animate is true (the default), and both
animate the same way: a paint-only FLIP slide from each affected
row's old painted position to its new one. Nothing enters or leaves
on either path, so neither produces per-row enter/exit animations.
- A cross-section reparent slides the moved row, timed by
slideDuration/slideCurve. - An in-section reorder slides every row whose position changed,
and only while the section is expanded: a reorder inside a
collapsed section has nothing on screen to slide. It does not
read
slideDuration/slideCurve, which name the moved row's own glide and have no single subject here.
Timing comes from the controller's expand/collapse spec
(animationStyle.expandCollapse) on both paths, so a move stays in
sync with the inserts, removes, expands and collapses it may compose
with inside one runBatch. Note the asymmetry the in-section path
inherits from reorderItems: zeroing reorderSlide disables its
slide even though the duration comes from expandCollapse.
Implementation
void moveItem(
K itemKey, {
K? toSection,
int? index,
bool animate = true,
Duration? slideDuration,
Curve? slideCurve,
}) {
_checkNotDisposed();
_requireItem(itemKey, "moveItem");
// Hoisted above the branch: the in-section path always refused a
// vanishing item, while the cross-section path fell through to
// moveNode, which CANCELS the deletion and resurrects it.
if (_tree.isPendingDeletion(ItemKey<K>(itemKey))) {
_throwMissing("moveItem", "item $itemKey is being removed");
}
if (toSection != null) {
_requireSection(toSection, "moveItem(toSection)");
// Documented as "appended" when [index] is null. moveNode treats a
// same-parent move with no index as a no-op, so name the last live
// slot explicitly; its own live-space guard keeps an already-last
// item a no-op.
final int? effectiveIndex =
index ??
(sectionOf(itemKey) == toSection
? _tree.liveChildCount(SectionKey<K>(toSection)) - 1
: null);
_tree.moveNode(
ItemKey<K>(itemKey),
SectionKey<K>(toSection),
index: effectiveIndex,
animate: animate,
slideDuration:
slideDuration ?? _tree.animationStyle.expandCollapse.duration,
slideCurve: slideCurve ?? _tree.animationStyle.expandCollapse.curve,
);
return;
}
if (index == null) {
// Neither a reparent target nor a reorder index — nothing to do.
return;
}
final parentKey = sectionOf(itemKey);
if (parentKey == null) {
_throwMissing("moveItem", "item $itemKey has no parent section");
}
// Use the LIVE list — `reorderItems` → `_tree.reorderChildren`
// validates against the live child set (excludes pending-deletion).
final siblings = itemKeysOf(parentKey)..remove(itemKey);
final clamped = index.clamp(0, siblings.length);
siblings.insert(clamped, itemKey);
// Forward [animate]. Dropping it here made `animate: false` slide
// anyway on this path only, which is what let the old doc claim this
// form "never animates regardless of animate" and stay unchallenged.
reorderItems(parentKey, siblings, animate: animate);
}