TreeController<TKey, TData> class

Controller for a SliverTree widget.

Manages:

  • Tree structure (nodes, parent/child relationships, depth)
  • Visibility (which nodes are in the flattened visible list)
  • Expansion state (which nodes are expanded)
  • Animation state (which nodes are animating and their progress)

Uses an ECS-style architecture where components are stored separately for efficient iteration and memory usage.

The controller provides two notification channels:

This separation allows the render object to only do full relayout when structure changes, and just update geometry/repaint during animations.

Inheritance

Constructors

TreeController({required TickerProvider vsync, TreeAnimationStyle animationStyle = const TreeAnimationStyle(), double indentWidth = 0.0, Comparator<TreeNode<TKey, TData>>? comparator})
Creates a tree controller.

Properties

anim → AnimationReader<TKey>
Public read-only accessor for the animation subsystem. Render-layer hot paths bind to this via AnimationReader (the narrow read interface implemented by AnimationCoordinator).
no setter
animationStyle ↔ TreeAnimationStyle
Animation timing/easing for every family: expand/collapse, enter/exit, reorder slides, make-room preview, drop settle.
getter/setter pair
bulkAnimationGeneration → int
Monotonic counter that bumps whenever the bulk animation group is created, destroyed, or its member set changes. The render object uses this to detect when its cached per-position offset cumulatives are stale.
no setter
bulkAnimationValue → double
Current animation value of the bulk animation group, or 0.0 if none.
no setter
comparator → Comparator<TreeNode<TKey, TData>>?
Optional comparator for maintaining sorted order among siblings.
final
composedSlideAbsDeltaBound → double
A true upper bound on the COMPOSED per-row delta, the SUM companion of maxActiveSlideAbsDelta's max. getSlideDeltaNid SUMS the FLIP engine and the make-room preview, and a row can carry both at once (a drag started while the prior commit's FLIP is still animating), reaching up to slideMax + previewMax; a max-based bound under-estimates exactly there. Use THIS bound wherever a window must provably contain every composed painted position (render overreach widening, the bounded drop-target scan). Degenerates to the single engine's max whenever the other is idle (an idle engine's max is 0.0), so single-engine states pay no extra width. Both engine maxima are computed on demand, never stale.
no setter
currentlyAnimatingKeys → Set<TKey>
Live, read-only view of every key currently animating across standalone, operation-group, and bulk sources. Backed by the lazy AnimationCoordinator.ensureAnimatingKeys cache; do NOT mutate , same convention as orderNidsView. Iteration is stable within one frame.
no setter
debugAllKeys → Iterable<TKey>
All currently-live keys, in nid order. Debug-only accessor for tests that need to pick a random key from the live set.
no setter
debugDescendantWalkCount ↔ int
Cumulative count of structural-descendant walks (_getDescendants). Perf pin companion to debugSlideBaselineStagingProbeCount: an idle expand must not materialize the descendant list at all (the staging row source is a thunk, invoked only past the gate), so the pair pins both halves of the idle-tap contract.
getter/setter pair
debugIndexInParentIterationCount ↔ int
Debug-only: cumulative count of sibling-probe loop iterations performed by the live-index cache refresh that backs getIndexInParent. Counts iterations rather than calls so tests can pin exact budgets: a cache-hit read costs ZERO iterations, and a refresh costs the touched list's FULL raw length (it rebuilds every live member's slot, not just the probed key's prefix). Never reset internally; tests zero it directly.
getter/setter pair
debugLastPreviewTargetIterationCount ↔ int
Debug-only: number of visible-order slots examined by the last setReorderPreview target computation. -1 when the computation has never run (calls that exit before the target loop leave it unchanged). Pins the loop's O(visibleNodeCount) contract, the order buffer is grow-only, so an unbounded loop would silently scan the stale capacity tail after a high-water mark.
getter/setter pair
debugOrderResetIndexAllCount → int
Number of full reverse-index resets (O(nidCapacity) memsets) the order buffer has performed. Perf oracle for the contiguous-removal fast path: incremental mutations must not trigger one.
no setter
debugPreviewInstallCount ↔ int
Debug-only: number of full target-map computations performed by setReorderPreview (memo misses). Never decremented. Pins the geometry memo's contract: re-resolving an unchanged slot at pointer frequency must not recompute the O(visibleNodeCount) target map.
getter/setter pair
debugSlideBaselineStagingProbeCount ↔ int
Cumulative count of per-row slide-delta probes performed by _stageSlideBaselineForBaseChange's any-live scan. Perf pin for the idle-tap contract: an expand/collapse with no composed slide activity anywhere performs ZERO probes (the O(1) hasActiveSlides gate returns first). Counts loop iterations, not calls. Tests reset it to 0 and assert an exact budget.
getter/setter pair
debugVisibleDescendantsWalkCount ↔ int
Cumulative count of visible-descendant walks (_getVisibleDescendants). Perf pin for collapse's single-walk contract: one collapse performs exactly ONE walk, shared by baseline staging and the removal logic.
getter/setter pair
hasActiveAnimations → bool
Whether any nodes are currently animating.
no setter
hasActiveFlipSlides → bool
Internal-use-only: whether any FLIP slide is in flight, EXCLUDING a held make-room preview.
no setter
hasActiveSlides → bool
Whether any FLIP slide animations are currently active.
no setter
hasActiveXSlides → bool
Whether any in-flight slide has a non-zero X-axis component (depth-changing reparent). Hot-path render code uses this to skip per-row X-delta reads when no X-axis work is in flight, the common case, since most reorders are same-depth.
no setter
hasComparator → bool
Whether this controller sorts siblings with a comparator.
no setter
hashCode → int
The hash code for this object.
no setterinherited
hasListeners → bool
Whether any listeners are currently registered.
no setterinherited
hasOpGroupAnimations → bool
Whether any non-bulk animations (operation groups or standalone) are currently active. When false and isBulkAnimating is true, the render object can use its scalar-offset fast path for the whole frame.
no setter
indentWidth ↔ double
Horizontal indent per depth level in logical pixels.
getter/setter pair
isBulkAnimating → bool
True when a bulk animation group is currently active and has members animating in either direction.
no setter
liveRootCount → int
Number of live (non-pending-deletion) roots. Non-allocating variant of liveRootKeys.length for hot paths (drop-zone boundary checks).
no setter
liveRootKeys → List<TKey>
Root keys that are not pending deletion.
no setter
maxActiveSlideAbsDelta → double
Test-only: the LARGER of the two engines' maxima (|currentDelta| ceilings), or 0.0 when both are idle.
no setter
nidCapacity → int
The current high-water mark for allocated nids. Nid-indexed dense arrays maintained externally should grow to at least this length.
no setter
orderNidsView → Int32List
Read-only view over the visible-order nid buffer for hot-path consumers that walk all visible positions and want to skip the per-position visibleNidAt dispatch. The underlying buffer's length may exceed visibleNodeCount, only the first N entries are valid. The buffer itself is mutated in place by structural changes; callers must not retain the reference across mutations.
no setter
rootCount → int
no setter
rootKeys → List<TKey>
Root node IDs in order.
latefinal
runtimeType → Type
A representation of the runtime type of the object.
no setterinherited
slideClampOverhangViewports ↔ double
Padding past the viewport edge (in viewport-heights) used as the start position (slide-IN) or end position (slide-OUT edge ghost) for FLIP slides whose prior or current is off-screen. A small overhang gives the row visible motion into / out of the viewport rather than appearing/disappearing exactly at the edge.
getter/setter pair
structureGeneration → int
no setter
visibleNodeCount → int
Number of visible nodes.
no setter
visibleNodes → List<TKey>
The flattened list of visible node IDs in render order.
latefinal

Methods

addAnimationListener(VoidCallback listener) → void
Registers a callback that fires on every animation tick.
addExpansionListener(void listener(TKey key, bool isExpanded)) → void
Registers a callback fired when a node's expansion state changes.
addListener(VoidCallback listener) → void
Register a closure to be called when the object changes.
inherited
addNodeDataListener(void listener(TKey key)) → void
Registers a callback that fires when a single node's data changes without any structural change (e.g. after updateNode).
addStructuralListener(void listener(Set<TKey>? affectedKeys)) → void
Registers a callback that fires on structural mutations with an optional set of affected keys. See _structuralListeners for the semantics of the argument.
animateDropSettleGlide(Map<TKey, ({double x, double y})> priorOffsets, Map<TKey, ({double x, double y})> currentOffsets, {required Duration duration, required Curve curve}) → void
Internal-use-only: installs a drop-settle glide (the drag proxy's cancel return / dead-commit-slide fallback).
animateScrollToKey(TKey key, {required ScrollController scrollController, Duration duration = const Duration(milliseconds: 300), Curve curve = Curves.linear, double alignment = 0.0, AncestorExpansionMode ancestorExpansion = AncestorExpansionMode.immediate, double extentEstimator(TKey key)?, double sliverBaseOffset = 0.0}) → Future<bool>
Animates scrollController to reveal key in its attached viewport.
animateSlideFromOffsets(Map<TKey, ({double x, double y})> priorOffsets, Map<TKey, ({double x, double y})> currentOffsets, {Duration? duration, Curve? curve, double maxSlideDistance = double.infinity}) → void
Starts a FLIP slide animation for every visible node whose position in scroll-space changed between priorOffsets (pre-mutation) and currentOffsets (post-mutation). Produce both with RenderSliverTree.snapshotVisibleOffsets, the first before the structural mutation, the second from inside a WidgetsBinding.addPostFrameCallback after the mutation's layout has run.
bulkAnimationData() → BulkAnimationData<TKey>
Captures every per-frame bulk-animation field the render object reads during a single layout into one snapshot. Forwards to AnimationCoordinator.bulkAnimationData which delegates to BulkAnimator.snapshot; the inactive case returns the const sentinel for zero per-call allocation.
captureOperationGroupToken(TKey operationKey) → Object?
Captures an opaque token identifying the current operation group at operationKey, or null if no group is installed there.
clearReorderPreview({required bool animate, Duration? duration, Curve? curve}) → void
Ends the make-room preview: animate the shifted rows back (animate true, pointer left every valid slot, or the drag was cancelled) or drop the offsets instantly (animate false, the commit path, where the staged FLIP baseline has already captured the shifted painted positions and the mutation's layout takes over).
collapse({required TKey key, bool animate = true}) → void
Collapses the given node, hiding its children.
collapseAll({bool animate = true, int? maxDepth}) → void
Collapses all nodes in the tree.
computeFirstAnimatingVisibleIndex() → int
Returns the smallest visible-order index among all currently-animating nodes, or visibleNodeCount when none are visible / none are animating.
debugAssertVisibleSubtreeSizeConsistency() → void
Debug-only forwarder to VisibleOrderBuffer.debugAssertSubtreeSizeConsistent, used by the fuzz and purge suites.
debugPrintBulkAnimationState() → void
Debug helper to print bulk animation state. Call this to verify animation is running correctly.
depthOfNid(int nid) → int
Depth for nid (0 for roots). No TKey hash. nid must be live.
dispose() → void
Discards any resources used by the object.
override
ensureAncestorsExpanded(TKey key) → int
Immediately expands every collapsed ancestor of key so that key becomes part of the visible order. Expansion is synchronous (no animation) so a subsequent scrollOffsetOf call sees the updated structure. Returns the number of ancestors that were expanded.
expand({required TKey key, bool animate = true}) → void
Expands the given node, revealing its children.
expandAll({bool animate = true, int? maxDepth}) → void
Expands all nodes in the tree.
extentOf(TKey key, {double extentEstimator(TKey key)?}) → double
Returns the best-known full (non-animated) extent for key: the measured value if the node has ever been laid out, otherwise extentEstimator if supplied, otherwise defaultExtent. Matches the fallback chain used by scrollOffsetOf.
getAnimatedExtent(TKey key, double fullExtent) → double
Gets the animated extent for a node. If the node is animating, returns the interpolated extent. Otherwise returns fullExtent.
getAnimationState(TKey key) → AnimationState?
Gets the animation state for a node, or null if not animating. Returns the standalone state if present, a synthetic entering state for operation group members that are expanding, or null for bulk/ collapsing groups.
getChildCount(TKey key) → int
Gets the number of children for the given node.
getChildren(TKey key) → List<TKey>
Gets the ordered list of child keys for the given node.
getCurrentExtent(TKey key) → double
Gets the current extent for a node, accounting for animation.
getCurrentExtentNid(int nid) → double
Current animated extent for the live nid. Hot-path forwarder to the AnimationCoordinator's nid-keyed read.
getDepth(TKey key) → int
Gets the depth of the given node (0 for roots).
getDescendants(TKey key) → List<TKey>
Returns all descendants of key in pre-order (children, grandchildren, ...). Does not include key itself. Returns an empty list if key has no children or is not present.
getEstimatedExtent(TKey key) → double
Gets the estimated full extent for a node. Returns the cached measured extent if available, otherwise defaultExtent.
getEstimatedExtentNid(int nid) → double
Estimated full extent for the live nid, measured value when available, defaultExtent otherwise. Hot-path equivalent of getEstimatedExtent that avoids the key-to-nid hash. Caller must guarantee nid is live and within range.
getFlipSlideDeltaNid(int nid) → double
Internal-use-only: FLIP-only Y slide delta for the live nid, EXCLUDING any held make-room preview offset. 0.0 when no FLIP slide is in flight for nid.
getIndent(TKey key) → double
Gets the horizontal indent for the given node.
getIndexInParent(TKey key) → int
Returns the zero-based index of key within the live sibling list of its parent (or the live root list, if key is a root). Returns -1 if key is not present or is itself pending deletion.
getLiveChildren(TKey parent) → List<TKey>
Children of parent that are not pending deletion.
getMeasuredExtent(TKey key) → double?
The measured full extent for key, or null if key has never been laid out. Distinct from getEstimatedExtent, which falls back to defaultExtent for unmeasured nodes.
getNodeData(TKey key) → TreeNode<TKey, TData>?
Gets the node data for the given key, or null if not found.
getParent(TKey key) → TKey?
Gets the parent key for the given node, or null if it is a root.
getSlideDelta(TKey key) → double
Current slide delta for key in scroll-space y, or 0.0 if the node is not currently sliding. Read by RenderSliverTree.paint, RenderSliverTree.applyPaintTransform, and the hit-test path on every frame (no caching, staleness-safe under tick-without-paint).
getSlideDeltaNid(int nid) → double
Slide delta for the live nid (paint-only FLIP offset), or 0.0 when the node is not currently sliding. Hot-path equivalent of getSlideDelta: read every paint, hit-test, and transform call for visible rows, so saving the key-to-nid hash matters.
getSlideDeltaX(TKey key) → double
X-axis (cross-axis indent) slide delta for key, or 0.0 when the node is not currently sliding (or not registered).
getSlideDeltaXNid(int nid) → double
X-axis (cross-axis indent) slide delta for the live nid, or 0.0 when the node is not currently sliding. Hot-path equivalent of getSlideDeltaX: read on every paint, hit-test, and transform call for visible rows during a depth-changing reparent.
getVisibleIndex(TKey key) → int
Gets the index of a node in the visible order, or -1 if not visible.
hasChildren(TKey key) → bool
Whether the given node has children.
hasLiveChildren(TKey parent) → bool
Whether parent has at least one live (non-pending-deletion) child.
insert({required TKey parentKey, required TreeNode<TKey, TData> node, int? index, bool animate = true, bool preservePendingSubtreeState = false}) → void
Inserts a new node as a child of the given parent.
insertRoot(TreeNode<TKey, TData> node, {int? index, bool animate = true, bool preservePendingSubtreeState = false}) → void
Adds a new root node to the tree.
isAnimating(TKey key) → bool
Whether the given node is currently animating. Forwards to the AnimationCoordinator's per-key check.
isAnimatingNid(int nid) → bool
Hot-path equivalent of isAnimating: O(1) array read via the AnimationCoordinator's union mirror. Caller must guarantee nid is live and within range.
isBulkMember(TKey key) → bool
Whether key is a member of the bulk animation group (either active or pending removal at animation end).
isExiting(TKey key) → bool
Whether the given node is currently exiting (animating out).
isExitingNid(int nid) → bool
Hot-path equivalent of isExiting.
isExpanded(TKey key) → bool
Whether the given node is expanded.
isOperationGroupSame(TKey operationKey, Object? token) → bool
Whether the operation group at operationKey is identity-equal to the one captured by captureOperationGroupToken.
isPendingDeletion(TKey key) → bool
Whether key is pending deletion, present in the structural maps but animating out and scheduled for purge once the animation settles.
isVisible(TKey key) → bool
Whether key is currently in the flattened visible order, i.e. every ancestor (if any) is expanded AND the node itself exists.
keyOfNid(int nid) → TKey?
Returns the key associated with nid, or null if the nid has been released. O(1). Consumers that cache nid-indexed state can use this to detect stale entries after node removal.
liveChildCount(TKey parent) → int
Number of live (non-pending-deletion) children of parent. Non-allocating variant of getLiveChildren(parent).length for hot paths (drop-zone boundary checks ask this per candidate level).
liveSiblingAt(TKey? parent, int index) → TKey?
The live sibling at index under parent (or among the roots when parent is null), or null when index is out of range.
markSlidePreserveProgress(TKey key) → void
Internal-use-only: marks the slide entry for key to bypass the engine's "un-touched re-baseline" branch on subsequent batch installs.
moveNode(TKey key, TKey? newParentKey, {int? index, bool animate = true, Duration? slideDuration, Curve? slideCurve}) → void
Moves a node from its current parent to newParentKey.
nidOf(TKey key) → int
Returns the internal nid for key, or noNid if the key isn't currently registered. O(1).
noSuchMethod(Invocation invocation) → dynamic
Invoked when a nonexistent method or property is accessed.
inherited
notifyAnimationListenersForScroll() → void
Forwards to the coordinator's listener channel so the scroll orchestrator's AnimationController (scrollProgress) can fire ticks through the controller's animation-listener channel.
notifyListeners() → void
Call all the registered listeners.
inherited
registerRenderHost(TreeRenderHost host) → void
Registers a render host. Called by RenderSliverTree.attach. Idempotent.
remove({required TKey key, bool animate = true}) → void
Removes a node and all its descendants from the tree.
removeAnimationListener(VoidCallback listener) → void
Removes a previously registered animation listener.
removeExpansionListener(void listener(TKey key, bool isExpanded)) → void
Removes a previously registered expansion listener.
removeListener(VoidCallback listener) → void
Remove a previously registered closure from the list of closures that are notified when the object changes.
inherited
removeNodeDataListener(void listener(TKey key)) → void
Removes a previously registered node-data listener.
removeStructuralListener(void listener(Set<TKey>? affectedKeys)) → void
Removes a previously registered structural listener.
reorderChildren(TKey parentKey, List<TKey> orderedKeys, {bool animate = true, Duration? slideDuration, Curve? slideCurve}) → void
Reorders the children of parentKey to match orderedKeys.
reorderRoots(List<TKey> orderedKeys, {bool animate = true, Duration? slideDuration, Curve? slideCurve}) → void
Reorders the root nodes to match orderedKeys.
runBatch<T>(T body()) → T
Runs body with structural notifications coalesced into a single notifyListeners call fired after body returns.
scrollOffsetOf(TKey key, {double extentEstimator(TKey key)?}) → double?
Returns the sliver-space scroll offset of key, or null if key is not in the current visible order (e.g., ancestors collapsed, or key not registered). The offset corresponds to the node's top edge within the SliverTree's own scroll extent.
setChildren(TKey parentKey, List<TreeNode<TKey, TData>> children) → void
Adds children to a node.
setFullExtent(TKey key, double extent) → void
Stores the measured full extent for a node.
setReorderPreview({required TKey draggedKey, required TKey targetKey, required bool gapBelowTarget, Duration? duration, Curve? curve}) → void
Opens the make-room preview at one of targetKey's own row edges: gapBelowTarget: false for the edge above it, true for the edge directly below it.
setReorderPreviewAtIndex({required TKey draggedKey, required int gapVisibleIndex, Duration? duration, Curve? curve}) → void
Opens (or re-targets) the paint-only make-room preview for a drag of draggedKey, with the gap opening BEFORE the row at gapVisibleIndex: rows at/after that slot shift down by the dragged subtree's visible extent, rows after the vacated slot shift up, and the offsets are HELD until this is called again (new slot) or clearReorderPreview releases them.
setRoots(List<TreeNode<TKey, TData>> roots) → void
Initializes the tree with the given root nodes.
takePendingExitPhantomAnchors() → Map<TKey, TKey>?
Returns and clears the staged exit-phantom relationships. Companion to takePendingPhantomAnchors for the visible-to-hidden case.
takePendingPhantomAnchors() → Map<TKey, TKey>?
Returns and clears the staged phantom-anchor relationships. Called by the render object during baseline consumption. Returns null when no relationships were staged (the common case, only animated reparents of hidden subtrees produce entries).
toggle({required TKey key, bool animate = true}) → void
Toggles the expansion state of the given node.
toString() → String
A string representation of this object.
inherited
unregisterRenderHost(TreeRenderHost host) → void
Unregisters a render host. Called by RenderSliverTree.detach. Tolerant of a host that was never registered (no-op).
updateNode(TreeNode<TKey, TData> node) → void
Updates the data payload for an existing node without structural changes.
visibleIndexOfNid(int nid) → int
Visible-order index for the live nid nid, or VisibleOrderBuffer.kNotVisible when nid is not currently in the visible order. O(1) typed-data read; no TKey hash. Hot-path equivalent of _order.indexByNid[nid].
visibleNidAt(int visibleIndex) → int
Returns the nid of the visible node at visibleIndex. No TKey hash occurs. Panics (unchecked read) if visibleIndex is out of range.
visibleSubtreeSize(TKey key) → int
Number of currently-visible rows in key's subtree, including key itself. O(1) read off the order buffer's cached subtree sizes.

Operators

operator ==(Object other) → bool
The equality operator.
inherited

Static Properties

debugFullConsistencyChecks ↔ bool
Opt-in: run the FULL cross-structure consistency sweep (whole order walk, nid-table walks, every animation mirror) after every incremental order mutation in debug builds. Off by default, the sweep makes N sequential inserts O(N^2) in debug; the default is an O(changed-range) order/reverse-index agreement check. The fuzz/purge suites, which exist to exercise the invariants, enable this.
getter/setter pair

Constants

defaultExtent → const double
Default extent for nodes that haven't been measured yet.
noNid → const int
Sentinel returned by nidOf when the key isn't registered. Same value as the internal VisibleOrderBuffer.kNotVisible but exposed separately since callers should treat it as "unknown key".