insert method
void
insert({})
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.
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;
}
final siblings = _childListOrCreate(parentKey);
final currentIndex = siblings.indexOf(node.key);
// Explicit index is live-space (compare live-vs-live and convert at
// the insert); the comparator path stays full-space end to end.
final int? sortedDesired = index == null && comparator != null
? _sortedIndex(siblings, node)
: null;
final bool wantsRelocate;
if (index != null) {
final currentLiveIndex = getIndexInParent(node.key);
final liveCount = _liveCountOf(siblings);
wantsRelocate =
index != currentLiveIndex &&
// Appending is a no-op if already live-last.
!(currentLiveIndex == liveCount - 1 && index >= liveCount);
} else if (sortedDesired != null) {
wantsRelocate =
sortedDesired != currentIndex &&
!(currentIndex == siblings.length - 1 &&
sortedDesired >= siblings.length);
} else {
wantsRelocate = false;
}
if (wantsRelocate) {
siblings.removeAt(currentIndex);
final insertAt = index != null
? _liveIndexToFullInsertIndex(siblings, index)
: sortedDesired!.clamp(0, siblings.length);
siblings.insert(insertAt, node.key);
// Raw sibling-list writes above; invalidate cached live indices.
_liveIndexCache.bump();
_markVisibleOrderDirty();
// Relocation changes row positions (and the payload was
// overwritten), structural refresh, which subsumes the data
// channel's row refresh, so the data channel does not also fire.
_notifyStructural(affectedKeys: <TKey>{node.key});
} else {
// Data-only update: fire the node-data channel only, matching
// updateNode's contract. Firing a structural notification too
// would refresh the same row twice.
_notifyNodeDataChanged(node.key);
}
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);
}
}
}
// 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: _siblingRefreshSet(parentKey));
}