moveItem method

void moveItem(
  1. K itemKey, {
  2. K? toSection,
  3. int? index,
  4. bool animate = true,
  5. Duration? slideDuration,
  6. Curve? slideCurve,
})

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);
}