insert method

void insert({
  1. required TKey parentKey,
  2. required TreeNode<TKey, TData> node,
  3. int? index,
  4. bool animate = true,
  5. bool preservePendingSubtreeState = false,
})

Inserts a new node as a child of the given parent.

If animate is true, the node will animate in.

index is the position among live siblings, exiting (pending-deletion) siblings are skipped, matching getLiveChildren / getIndexInParent and the input space of reorderChildren.

preservePendingSubtreeState controls what happens to a re-added mid-exit node's in-flight removal subtree: false (the default) discards it, so the node comes back alone, matching remove(animate: false) followed by a fresh add; true restores it. A discarded descendant whose exit animation is still running is removed when that exit lands, not at the instant of the re-add, so the raw getChildren view still lists it until then.

Throws a StateError if parentKey is pending deletion (animating out): the parent will be purged when its exit animation completes, which would leave the new child orphaned.

Implementation

void insert({
  required TKey parentKey,
  required TreeNode<TKey, TData> node,
  int? index,
  bool animate = true,
  bool preservePendingSubtreeState = false,
}) {
  if (_animationStyle.effectiveEnterExit.duration == Duration.zero) {
    animate = false;
  }
  assert(_hasKey(parentKey), "Parent node $parentKey not found");
  // Flush any pending visible-order rebuild from a prior in-batch mutator
  // (moveNode, reorderRoots, reorderChildren, cancelDeletion, and so
  // on). Without
  // this, the `_order.indexOf(parentKey)` / `_order.subtreeSizeOf(parentNid)`
  // reads further down would return stale values and yield an insertIndex
  // past `_order.length`, crashing `VisibleOrderBuffer.insertNid` with a
  // RangeError on its setRange shift. Must run BEFORE the mutations below
  // (`_adoptKey`, `siblings.add`, etc.) so the rebuild sees pre-mutation
  // child lists and doesn't try to incorporate the new node twice.
  _ensureVisibleOrder();
  // Runtime check (not just an assert) so release builds also reject
  // this rather than silently corrupting state: a child inserted under
  // a mid-exit parent survives the parent's purge with a dangling
  // parent nid and leaks its registry entry. Mirrors moveNode's policy.
  if (_isPendingDeletion(parentKey)) {
    throw StateError(
      "Cannot insert under $parentKey while it is animating out "
      "(pending deletion). The parent will be purged when its exit "
      "animation completes, leaving the new child orphaned.",
    );
  }
  // If the node is pending deletion, cancel the deletion
  if (_isPendingDeletion(node.key)) {
    // If the pending-deletion node lives under a different parent (or is
    // a root), move it to [parentKey] before cancelling the deletion.
    // Without this relocation, cancelDeletion would resurrect the node
    // under its old parent, silently ignoring the parentKey/index args.
    final oldParent = _parentKeyOfKey(node.key);
    if (oldParent != parentKey) {
      if (oldParent != null) {
        _childListOf(oldParent)?.remove(node.key);
      } else {
        _roots.remove(node.key);
      }
      _setParentKey(node.key, parentKey);
      final siblings = _childListOrCreate(parentKey);
      // Explicit index is live-space; the comparator path already
      // returns a full-space position.
      final effectiveIndex = index != null
          ? _liveIndexToFullInsertIndex(siblings, index)
          : (comparator != null ? _sortedIndex(siblings, node) : null);
      if (effectiveIndex != null && effectiveIndex < siblings.length) {
        siblings.insert(effectiveIndex, node.key);
      } else {
        siblings.add(node.key);
      }
      final parentDepth = _depthOfKey(parentKey);
      _refreshSubtreeDepths(node.key, parentDepth + 1);
      // Raw sibling-list writes above; invalidate cached live indices.
      _liveIndexCache.bump();
    } else if (index != null) {
      // Same parent: honor an explicitly requested index by relocating
      // within the sibling list. The index is live-space; convert after
      // the removal so the conversion sees the list the insert will
      // apply to.
      final siblings = _childListOrCreate(parentKey);
      final current = siblings.indexOf(node.key);
      if (current != -1) {
        siblings.removeAt(current);
        final fullIndex = _liveIndexToFullInsertIndex(siblings, index);
        siblings.insert(fullIndex, node.key);
      }
    }
    _cancelDeletion(
      node.key,
      animate: animate,
      preserveSubtreeState: preservePendingSubtreeState,
    );
    _adoptKey(node.key);
    _store.setData(node.key, node);
    // Same as insertRoot's matching branch, fire the node-data channel
    // so listeners subscribed via [addNodeDataListener] see the data
    // update.
    _notifyNodeDataChanged(node.key);
    if (preservePendingSubtreeState) {
      _markVisibleOrderDirty();
      // See insertRoot's matching branch: cancelling a pending deletion
      // may restore a subtree and flip ancestor hasChildren state, fall
      // back to a full refresh.
      _notifyStructural();
      return;
    }
    // Reset expansion state so a subsequent expand() works cleanly.
    // Descendants that were mid-exit are left alone by _cancelDeletion
    // and continue animating out under the restored parent via
    // _rebuildVisibleOrder's "collapsed with active animations" branch.
    // Yanking them here would visually jump following rows upward by
    // the descendant's current extent in a single frame.
    _setExpandedKey(node.key, false);
    _markVisibleOrderDirty();
    _notifyStructural();
    return;
  }
  // Node is already present (e.g. restored by an ancestor's
  // _cancelDeletion, or a live re-insert). Update the data and, if the
  // caller requested a different location, relocate it to honor the
  // insert(parentKey:, index:) contract instead of silently dropping it.
  if (_hasKey(node.key)) {
    _adoptKey(node.key);
    _store.setData(node.key, node);
    final currentParent = _parentKeyOfKey(node.key);
    if (currentParent != parentKey) {
      // Fire the node-data channel BEFORE delegating. moveNode's
      // structural notification is targeted, on a depth-preserving
      // move its affectedKeys omits the moved key itself, so this is
      // the only refresh path for the overwritten payload.
      _notifyNodeDataChanged(node.key);
      // Different parent: delegate to moveNode. Forward the caller's
      // `animate` so insert(animate: false) doesn't silently slide
      // (now that moveNode itself defaults to animate: true).
      moveNode(node.key, parentKey, index: index, animate: animate);
      return;
    }
    _relocateOrUpdateExistingSibling(parentKey, node, index);
    return;
  }
  final parentDepth = _depthOfKey(parentKey);
  // Add to data structures
  _adoptKey(node.key);
  _store.setData(node.key, node);
  _setParentKey(node.key, parentKey);
  _setChildList(node.key, []);
  _setDepthKey(node.key, parentDepth + 1);
  _setExpandedKey(node.key, false);
  // Add to parent's children. Explicit index is live-space; the
  // comparator path already returns a full-space position.
  final siblings = _childListOrCreate(parentKey);
  final effectiveIndex = index != null
      ? _liveIndexToFullInsertIndex(siblings, index)
      : (comparator != null ? _sortedIndex(siblings, node) : null);
  if (effectiveIndex != null && effectiveIndex < siblings.length) {
    siblings.insert(effectiveIndex, node.key);
  } else {
    siblings.add(node.key);
  }
  // Raw sibling-list writes above; invalidate cached live indices.
  _liveIndexCache.bump();
  // If parent is expanded, add to visible order
  if (_isExpandedKey(parentKey)) {
    final parentVisibleIndex = _order.indexOf(parentKey);
    if (parentVisibleIndex != VisibleOrderBuffer.kNotVisible) {
      // Fast path: the visible insertion index equals the parent's
      // visible index plus every prior sibling's visible-subtree
      // contribution. Cache lookups are O(1) per sibling, so the
      // whole computation is O(prior-sibling-count), one array
      // read per sibling, no nested descendant walks.
      int insertIndex;
      if (effectiveIndex != null) {
        insertIndex = parentVisibleIndex + 1;
        final limit = effectiveIndex < siblings.length - 1
            ? effectiveIndex
            : siblings.length - 1;
        for (int i = 0; i < limit; i++) {
          final siblingNid = _nids[siblings[i]];
          if (siblingNid != null) {
            insertIndex += _order.subtreeSizeOf(siblingNid);
          }
        }
      } else {
        // Append after last visible descendant of parent. Parent is
        // visible (checked above) so its cached subtree size counts
        // itself + all currently-visible descendants; subtracting
        // 1 for the parent itself and adding 1 for "position after"
        // yields parentVisibleIndex + subtreeSize directly.
        final parentNid = _nids[parentKey]!;
        insertIndex = parentVisibleIndex + _order.subtreeSizeOf(parentNid);
      }
      _order.insertKey(insertIndex, node.key);
      _updateIndicesFrom(insertIndex);
      _structureGeneration++;
      if (animate) {
        _startStandaloneEnterAnimation(
          node.key,
          family: TreeAnimationFamily.enterExit,
        );
      }
    }
  }
  // The new key enters via createChild. The retained rows that can
  // change are the parent (its child count grew, and its hasChildren may
  // have flipped) and every sibling the insert displaced.
  _notifyStructural(affectedKeys: _siblingRefreshOrToken(parentKey));
}