buildScope method

void buildScope(
  1. Element context,
  2. [VoidCallback? callback]
)

Establishes a scope for updating the widget tree, and calls the given callback, if any. Then, builds all the elements that were marked as dirty using scheduleBuildFor, in depth order.

This mechanism prevents build methods from transitively requiring other build methods to run, potentially causing infinite loops.

The dirty list is processed after callback returns, building all the elements that were marked as dirty using scheduleBuildFor, in depth order. If elements are marked as dirty while this method is running, they must be deeper than the context node, and deeper than any previously-built node in this pass.

To flush the current dirty list without performing any other work, this function can be called with no callback. This is what the framework does each frame, in WidgetsBinding.drawFrame.

Only one buildScope can be active at a time.

A buildScope implies a lockState scope as well.

To print a console message every time this method is called, set debugPrintBuildScope to true. This is useful when debugging problems involving widgets not getting marked dirty, or getting marked dirty too often.

Implementation

@pragma('vm:notify-debugger-on-exception')
void buildScope(Element context, [ VoidCallback? callback ]) {
  if (callback == null && _dirtyElements.isEmpty) {
    return;
  }
  assert(_debugStateLockLevel >= 0);
  assert(!_debugBuilding);
  assert(() {
    if (debugPrintBuildScope) {
      debugPrint('buildScope called with context $context; dirty list is: $_dirtyElements');
    }
    _debugStateLockLevel += 1;
    _debugBuilding = true;
    return true;
  }());
  if (!kReleaseMode) {
    Map<String, String>? debugTimelineArguments;
    assert(() {
      if (debugEnhanceBuildTimelineArguments) {
        debugTimelineArguments = <String, String>{
          'dirty count': '${_dirtyElements.length}',
          'dirty list': '$_dirtyElements',
          'lock level': '$_debugStateLockLevel',
          'scope context': '$context',
        };
      }
      return true;
    }());
    FlutterTimeline.startSync(
      'BUILD',
      arguments: debugTimelineArguments
    );
  }
  try {
    _scheduledFlushDirtyElements = true;
    if (callback != null) {
      assert(_debugStateLocked);
      Element? debugPreviousBuildTarget;
      assert(() {
        debugPreviousBuildTarget = _debugCurrentBuildTarget;
        _debugCurrentBuildTarget = context;
        return true;
      }());
      _dirtyElementsNeedsResorting = false;
      try {
        callback();
      } finally {
        assert(() {
          assert(_debugCurrentBuildTarget == context);
          _debugCurrentBuildTarget = debugPreviousBuildTarget;
          _debugElementWasRebuilt(context);
          return true;
        }());
      }
    }
    _dirtyElements.sort(Element._sort);
    _dirtyElementsNeedsResorting = false;
    int dirtyCount = _dirtyElements.length;
    int index = 0;
    while (index < dirtyCount) {
      final Element element = _dirtyElements[index];
      assert(element._inDirtyList);
      assert(() {
        if (element._lifecycleState == _ElementLifecycle.active && !element._debugIsInScope(context)) {
          throw FlutterError.fromParts(<DiagnosticsNode>[
            ErrorSummary('Tried to build dirty widget in the wrong build scope.'),
            ErrorDescription(
              'A widget which was marked as dirty and is still active was scheduled to be built, '
              'but the current build scope unexpectedly does not contain that widget.',
            ),
            ErrorHint(
              'Sometimes this is detected when an element is removed from the widget tree, but the '
              'element somehow did not get marked as inactive. In that case, it might be caused by '
              'an ancestor element failing to implement visitChildren correctly, thus preventing '
              'some or all of its descendants from being correctly deactivated.',
            ),
            DiagnosticsProperty<Element>(
              'The root of the build scope was',
              context,
              style: DiagnosticsTreeStyle.errorProperty,
            ),
            DiagnosticsProperty<Element>(
              'The offending element (which does not appear to be a descendant of the root of the build scope) was',
              element,
              style: DiagnosticsTreeStyle.errorProperty,
            ),
          ]);
        }
        return true;
      }());
      final bool isTimelineTracked = !kReleaseMode && _isProfileBuildsEnabledFor(element.widget);
      if (isTimelineTracked) {
        Map<String, String>? debugTimelineArguments;
        assert(() {
          if (kDebugMode && debugEnhanceBuildTimelineArguments) {
            debugTimelineArguments = element.widget.toDiagnosticsNode().toTimelineArguments();
          }
          return true;
        }());
        FlutterTimeline.startSync(
          '${element.widget.runtimeType}',
          arguments: debugTimelineArguments,
        );
      }
      try {
        element.rebuild();
      } catch (e, stack) {
        _reportException(
          ErrorDescription('while rebuilding dirty elements'),
          e,
          stack,
          informationCollector: () => <DiagnosticsNode>[
            if (kDebugMode && index < _dirtyElements.length)
              DiagnosticsDebugCreator(DebugCreator(element)),
            if (index < _dirtyElements.length)
              element.describeElement('The element being rebuilt at the time was index $index of $dirtyCount')
            else
              ErrorHint('The element being rebuilt at the time was index $index of $dirtyCount, but _dirtyElements only had ${_dirtyElements.length} entries. This suggests some confusion in the framework internals.'),
          ],
        );
      }
      if (isTimelineTracked) {
        FlutterTimeline.finishSync();
      }
      index += 1;
      if (dirtyCount < _dirtyElements.length || _dirtyElementsNeedsResorting!) {
        _dirtyElements.sort(Element._sort);
        _dirtyElementsNeedsResorting = false;
        dirtyCount = _dirtyElements.length;
        while (index > 0 && _dirtyElements[index - 1].dirty) {
          // It is possible for previously dirty but inactive widgets to move right in the list.
          // We therefore have to move the index left in the list to account for this.
          // We don't know how many could have moved. However, we do know that the only possible
          // change to the list is that nodes that were previously to the left of the index have
          // now moved to be to the right of the right-most cleaned node, and we do know that
          // all the clean nodes were to the left of the index. So we move the index left
          // until just after the right-most clean node.
          index -= 1;
        }
      }
    }
    assert(() {
      if (_dirtyElements.any((Element element) => element._lifecycleState == _ElementLifecycle.active && element.dirty)) {
        throw FlutterError.fromParts(<DiagnosticsNode>[
          ErrorSummary('buildScope missed some dirty elements.'),
          ErrorHint('This probably indicates that the dirty list should have been resorted but was not.'),
          Element.describeElements('The list of dirty elements at the end of the buildScope call was', _dirtyElements),
        ]);
      }
      return true;
    }());
  } finally {
    for (final Element element in _dirtyElements) {
      assert(element._inDirtyList);
      element._inDirtyList = false;
    }
    _dirtyElements.clear();
    _scheduledFlushDirtyElements = false;
    _dirtyElementsNeedsResorting = null;
    if (!kReleaseMode) {
      FlutterTimeline.finishSync();
    }
    assert(_debugBuilding);
    assert(() {
      _debugBuilding = false;
      _debugStateLockLevel -= 1;
      if (debugPrintBuildScope) {
        debugPrint('buildScope finished');
      }
      return true;
    }());
  }
  assert(_debugStateLockLevel >= 0);
}