runBatch<T> method

T runBatch<T>(
  1. T body()
)

Runs body with structural notifications coalesced into a single notifyListeners call fired after body returns.

Any number of mutations inside bodyinsertRoot, insert, remove, expand, collapse, updateNode, moveNode, etc. — fire at most one structural notification when the outermost runBatch exits. Nested runBatch calls coalesce into the outermost one.

Per-channel batching contract:

  • Structural (addStructuralListener / notifyListeners): deferred. Fires once on batch exit with the union of affected keys.
  • Node-data (addNodeDataListener): deferred. Fires once per dirty key on batch exit, after structural. A key that was both structurally and data-mutated triggers both notifications.
  • Animation tick (addAnimationListener): NOT deferred. These fire on the next animation vsync frame (their natural schedule) and are unaffected by batching either way.

The notification fires even if body throws, so listeners always see the post-batch state. Exceptions propagate after the notification.

Implementation

T runBatch<T>(T Function() body) {
  _batchDepth++;
  try {
    return body();
  } finally {
    _batchDepth--;
    if (_batchDepth == 0) {
      // Flush any deferred visible-order rebuild BEFORE notifications
      // fire — listeners reading visibleNodes/orderNidsView in their
      // callback must see the post-batch state. Cheap when no
      // mutation flagged dirtiness (single bool check).
      _ensureVisibleOrder();
      final didStructural = _batchDidRequestStructural;
      final dirtyData = _batchDirtyDataNodes;
      final structuralAffected = _batchAffectedStructuralUnknown
          ? null
          : _batchAffectedStructuralKeys;
      _batchDidRequestStructural = false;
      _batchDirtyDataNodes = null;
      _batchAffectedStructuralKeys = null;
      _batchAffectedStructuralUnknown = false;
      // Fire structural first: a structural notify causes the element to
      // mark itself for a full refresh, which subsumes any data-only
      // refresh for the same keys. Firing data first would queue a
      // targeted refresh that the full refresh then redundantly repeats.
      if (didStructural) {
        _fireStructuralListeners(structuralAffected);
        notifyListeners();
      }
      if (dirtyData != null && dirtyData.isNotEmpty) {
        for (final key in dirtyData) {
          _fireNodeDataListeners(key);
        }
      }
    }
  }
}