Reverts "Prevent LayoutBuilder from rebuilding more than once (#147856)" (#149279)

Reverts: flutter/flutter#147856
Initiated by: loic-sharma
Reason for reverting: tree is closed with errors like: 

```
test/integration.shard/break_on_framework_exceptions_test.dart: breaks when rebuilding dirty elements throws [E]
  Expected: <45>
    Actual: <2756>
  
  package:matcher                                                       expect
  test\integration.shard\break_on_framework_exceptions_test.dart 56:5   main.expectException
  ===== asynchronous gap ===
Original PR Author: LongCatIsLooong

Reviewed By: {goderbauer}

This change reverts the following previous change:
Fixes https://github.com/flutter/flutter/issues/146379: introduces `Element.buildScope` which `BuildOwner.buildScope` uses to identify subtrees that need skipping (those with different `BuildScope`s). If `Element.update` calls `updateChild` then dirty children will still be rebuilt regardless of their build scopes. 

This also introduces `LayoutBuilder.applyDoubleRebuildFix` migration flag which should only live for a week or less. 

Caveats: 

`LayoutBuilder`'s render object calls `markNeedsLayout` if a descendant Element is dirty. Since `markNeedsLayout` also implies `markNeedsPaint`, the render object is going to be very repaint/relayout-happy.

Tests: 

Presubmits with the migration flag set to true: https://github.com/flutter/flutter/pull/147856/checks?check_run_id=24629865893
This commit is contained in:
auto-submit[bot] 2024-05-29 23:29:19 +00:00 committed by GitHub
parent 182c1e6a36
commit ebc414337d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 248 additions and 712 deletions

View File

@ -1999,7 +1999,8 @@ abstract class RenderObject with DiagnosticableTreeMixin implements HitTestTarge
}
if (!activeLayoutRoot._debugMutationsLocked) {
activeLayoutRoot = activeLayoutRoot.debugLayoutParent;
final RenderObject? p = activeLayoutRoot.debugLayoutParent;
activeLayoutRoot = p is RenderObject ? p : null;
} else {
// activeLayoutRoot found.
break;
@ -3003,7 +3004,7 @@ abstract class RenderObject with DiagnosticableTreeMixin implements HitTestTarge
owner!._nodesNeedingPaint.add(this);
owner!.requestVisualUpdate();
}
} else if (parent != null) {
} else if (parent is RenderObject) {
parent!.markNeedsPaint();
} else {
assert(() {
@ -3019,7 +3020,9 @@ abstract class RenderObject with DiagnosticableTreeMixin implements HitTestTarge
//
// Trees rooted at a RenderView do not go through this
// code path because RenderViews are repaint boundaries.
owner?.requestVisualUpdate();
if (owner != null) {
owner!.requestVisualUpdate();
}
}
}

View File

@ -2610,205 +2610,6 @@ abstract class BuildContext {
DiagnosticsNode describeOwnershipChain(String name);
}
/// A class that determines the scope of a [BuildOwner.buildScope] operation.
///
/// The [BuildOwner.buildScope] method rebuilds all dirty [Element]s who share
/// the same [Element.buildScope] as its `context` argument, and skips those
/// with a different [Element.buildScope].
///
/// [Element]s by default have the same `buildScope` as their parents. Special
/// [Element]s may override [Element.buildScope] to create an isolated build scope
/// for its descendants. The [LayoutBuilder] widget, for example, establishes its
/// own [BuildScope] such that no descendant [Element]s may rebuild prematurely
/// until the incoming constraints are known.
final class BuildScope {
/// Creates a [BuildScope] with an optional [scheduleRebuild] callback.
BuildScope({ this.scheduleRebuild });
// Whether `scheduleRebuild` is called.
bool _buildScheduled = false;
// Whether [BuildOwner.buildScope] is actively running in this [BuildScope].
bool _building = false;
/// An optional [VoidCallback] that will be called when [Element]s in this
/// [BuildScope] are marked as dirty for the first time.
///
/// This callback usually signifies that the [BuildOwner.buildScope] method
/// must be called at a later time in this frame to rebuild dirty elements in
/// this [BuildScope]. It will **not** be called if this scope is actively being
/// built by [BuildOwner.buildScope], since the [BuildScope] will be clean when
/// [BuildOwner.buildScope] returns.
final VoidCallback? scheduleRebuild;
/// Whether [_dirtyElements] need to be sorted again as a result of more
/// elements becoming dirty during the build.
///
/// This is necessary to preserve the sort order defined by [Element._sort].
///
/// This field is set to null when [buildScope] is not actively rebuilding
/// the widget tree.
bool? _dirtyElementsNeedsResorting;
final List<Element> _dirtyElements = <Element>[];
@pragma('dart2js:tryInline')
@pragma('vm:prefer-inline')
@pragma('wasm:prefer-inline')
void _scheduleBuildFor(Element element) {
assert(identical(element.buildScope, this));
if (!element._inDirtyList) {
_dirtyElements.add(element);
element._inDirtyList = true;
}
if (!_buildScheduled && !_building) {
_buildScheduled = true;
scheduleRebuild?.call();
}
if (_dirtyElementsNeedsResorting != null) {
_dirtyElementsNeedsResorting = true;
}
}
@pragma('dart2js:tryInline')
@pragma('vm:prefer-inline')
@pragma('wasm:prefer-inline')
void _tryRebuild(Element element) {
assert(element._inDirtyList);
assert(identical(element.buildScope, this));
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)
DiagnosticsDebugCreator(DebugCreator(element)),
element.describeElement('The element being rebuilt at the time was')
],
);
}
if (isTimelineTracked) {
FlutterTimeline.finishSync();
}
}
bool _debugAssertElementInScope(Element element, Element debugBuildRoot) {
final bool isInScope = element._debugIsDescsendantOf(debugBuildRoot)
|| !element.debugIsActive;
if (isInScope) {
return true;
}
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',
debugBuildRoot,
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,
),
]);
}
void _flushDirtyElements({ required Element debugBuildRoot }) {
assert(_dirtyElementsNeedsResorting == null, '_flushDirtyElements must be non-reentrant');
_dirtyElements.sort(Element._sort);
_dirtyElementsNeedsResorting = false;
try {
for (int index = 0; index < _dirtyElements.length; index = _dirtyElementIndexAfter(index)) {
final Element element = _dirtyElements[index];
if (identical(element.buildScope, this)) {
assert(_debugAssertElementInScope(element, debugBuildRoot));
_tryRebuild(element);
}
}
assert(() {
final Iterable<Element> missedElements = _dirtyElements.where((Element element) => element.debugIsActive && element.dirty && identical(element.buildScope, this));
if (missedElements.isNotEmpty) {
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.'),
DiagnosticsProperty<Element>(
'The context argument of the buildScope call was',
debugBuildRoot,
style: DiagnosticsTreeStyle.errorProperty,
),
Element.describeElements('The list of missed elements at the end of the buildScope call was', missedElements),
]);
}
return true;
}());
} finally {
for (final Element element in _dirtyElements) {
if (identical(element.buildScope, this)) {
element._inDirtyList = false;
}
}
_dirtyElements.clear();
_dirtyElementsNeedsResorting = null;
_buildScheduled = false;
}
}
@pragma('dart2js:tryInline')
@pragma('vm:prefer-inline')
@pragma('wasm:prefer-inline')
int _dirtyElementIndexAfter(int index) {
if (!_dirtyElementsNeedsResorting!) {
return index + 1;
}
index += 1;
_dirtyElements.sort(Element._sort);
_dirtyElementsNeedsResorting = false;
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(() {
for (int i = index - 1; i >= 0; i -= 1) {
final Element element = _dirtyElements[i];
assert(!element.dirty || element._lifecycleState != _ElementLifecycle.active);
}
return true;
}());
return index;
}
}
/// Manager class for the widgets framework.
///
/// This class tracks which widgets need rebuilding, and handles other tasks
@ -2850,8 +2651,23 @@ class BuildOwner {
final _InactiveElements _inactiveElements = _InactiveElements();
final List<Element> _dirtyElements = <Element>[];
bool _scheduledFlushDirtyElements = false;
/// Whether [_dirtyElements] need to be sorted again as a result of more
/// elements becoming dirty during the build.
///
/// This is necessary to preserve the sort order defined by [Element._sort].
///
/// This field is set to null when [buildScope] is not actively rebuilding
/// the widget tree.
bool? _dirtyElementsNeedsResorting;
/// Whether [buildScope] is actively rebuilding the widget tree.
///
/// [scheduleBuildFor] should only be called when this value is true.
bool get _debugIsInBuildScope => _dirtyElementsNeedsResorting != null;
/// The object in charge of the focus tree.
///
/// Rarely used directly. Instead, consider using [FocusScope.of] to obtain
@ -2870,10 +2686,9 @@ class BuildOwner {
/// when [WidgetsBinding.drawFrame] calls [buildScope].
void scheduleBuildFor(Element element) {
assert(element.owner == this);
assert(element._parentBuildScope != null);
assert(() {
if (debugPrintScheduleBuildForStacks) {
debugPrintStack(label: 'scheduleBuildFor() called for $element${element.buildScope._dirtyElements.contains(element) ? " (ALREADY IN LIST)" : ""}');
debugPrintStack(label: 'scheduleBuildFor() called for $element${_dirtyElements.contains(element) ? " (ALREADY IN LIST)" : ""}');
}
if (!element.dirty) {
throw FlutterError.fromParts(<DiagnosticsNode>[
@ -2892,36 +2707,34 @@ class BuildOwner {
}
return true;
}());
final BuildScope buildScope = element.buildScope;
assert(() {
if (debugPrintScheduleBuildForStacks && element._inDirtyList) {
debugPrintStack(
label: 'BuildOwner.scheduleBuildFor() called; '
'_dirtyElementsNeedsResorting was ${buildScope._dirtyElementsNeedsResorting} (now true); '
'The dirty list for the current build scope is: ${buildScope._dirtyElements}',
);
}
// When reactivating an inactivate Element, _scheduleBuildFor should only be
// called within _flushDirtyElements.
if (!_debugBuilding && element._inDirtyList) {
throw FlutterError.fromParts(<DiagnosticsNode>[
ErrorSummary('BuildOwner.scheduleBuildFor() called inappropriately.'),
ErrorHint(
'The BuildOwner.scheduleBuildFor() method should only be called while the '
'buildScope() method is actively rebuilding the widget tree.',
),
]);
}
return true;
}());
if (element._inDirtyList) {
assert(() {
if (debugPrintScheduleBuildForStacks) {
debugPrintStack(label: 'BuildOwner.scheduleBuildFor() called; _dirtyElementsNeedsResorting was $_dirtyElementsNeedsResorting (now true); dirty list is: $_dirtyElements');
}
if (!_debugIsInBuildScope) {
throw FlutterError.fromParts(<DiagnosticsNode>[
ErrorSummary('BuildOwner.scheduleBuildFor() called inappropriately.'),
ErrorHint(
'The BuildOwner.scheduleBuildFor() method should only be called while the '
'buildScope() method is actively rebuilding the widget tree.',
),
]);
}
return true;
}());
_dirtyElementsNeedsResorting = true;
return;
}
if (!_scheduledFlushDirtyElements && onBuildScheduled != null) {
_scheduledFlushDirtyElements = true;
onBuildScheduled!();
}
buildScope._scheduleBuildFor(element);
_dirtyElements.add(element);
element._inDirtyList = true;
assert(() {
if (debugPrintScheduleBuildForStacks) {
debugPrint("...the build scope's dirty list is now: $buildScope._dirtyElements");
debugPrint('...dirty list is now: $_dirtyElements');
}
return true;
}());
@ -2986,18 +2799,14 @@ class BuildOwner {
/// often.
@pragma('vm:notify-debugger-on-exception')
void buildScope(Element context, [ VoidCallback? callback ]) {
final BuildScope buildScope = context.buildScope;
if (callback == null && buildScope._dirtyElements.isEmpty) {
if (callback == null && _dirtyElements.isEmpty) {
return;
}
assert(_debugStateLockLevel >= 0);
assert(!_debugBuilding);
assert(() {
if (debugPrintBuildScope) {
debugPrint(
'buildScope called with context $context; '
"its build scope's dirty list is: ${buildScope._dirtyElements}",
);
debugPrint('buildScope called with context $context; dirty list is: $_dirtyElements');
}
_debugStateLockLevel += 1;
_debugBuilding = true;
@ -3008,8 +2817,8 @@ class BuildOwner {
assert(() {
if (debugEnhanceBuildTimelineArguments) {
debugTimelineArguments = <String, String>{
'build scope dirty count': '${buildScope._dirtyElements.length}',
'build scope dirty list': '${buildScope._dirtyElements}',
'dirty count': '${_dirtyElements.length}',
'dirty list': '$_dirtyElements',
'lock level': '$_debugStateLockLevel',
'scope context': '$context',
};
@ -3023,7 +2832,6 @@ class BuildOwner {
}
try {
_scheduledFlushDirtyElements = true;
buildScope._building = true;
if (callback != null) {
assert(_debugStateLocked);
Element? debugPreviousBuildTarget;
@ -3032,6 +2840,7 @@ class BuildOwner {
_debugCurrentBuildTarget = context;
return true;
}());
_dirtyElementsNeedsResorting = false;
try {
callback();
} finally {
@ -3043,10 +2852,110 @@ class BuildOwner {
}());
}
}
buildScope._flushDirtyElements(debugBuildRoot: context);
_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 {
buildScope._building = false;
for (final Element element in _dirtyElements) {
assert(element._inDirtyList);
element._inDirtyList = false;
}
_dirtyElements.clear();
_scheduledFlushDirtyElements = false;
_dirtyElementsNeedsResorting = null;
if (!kReleaseMode) {
FlutterTimeline.finishSync();
}
@ -3066,8 +2975,9 @@ class BuildOwner {
Map<Element, Set<GlobalKey>>? _debugElementsThatWillNeedToBeRebuiltDueToGlobalKeyShenanigans;
void _debugTrackElementThatWillNeedToBeRebuiltDueToGlobalKeyShenanigans(Element node, GlobalKey key) {
final Map<Element, Set<GlobalKey>> map = _debugElementsThatWillNeedToBeRebuiltDueToGlobalKeyShenanigans ??= HashMap<Element, Set<GlobalKey>>();
final Set<GlobalKey> keys = map.putIfAbsent(node, () => HashSet<GlobalKey>());
_debugElementsThatWillNeedToBeRebuiltDueToGlobalKeyShenanigans ??= HashMap<Element, Set<GlobalKey>>();
final Set<GlobalKey> keys = _debugElementsThatWillNeedToBeRebuiltDueToGlobalKeyShenanigans!
.putIfAbsent(node, () => HashSet<GlobalKey>());
keys.add(key);
}
@ -3267,7 +3177,8 @@ class BuildOwner {
try {
_debugVerifyGlobalKeyReservation();
_debugVerifyIllFatedPopulation();
if (_debugElementsThatWillNeedToBeRebuiltDueToGlobalKeyShenanigans?.isNotEmpty ?? false) {
if (_debugElementsThatWillNeedToBeRebuiltDueToGlobalKeyShenanigans != null &&
_debugElementsThatWillNeedToBeRebuiltDueToGlobalKeyShenanigans!.isNotEmpty) {
final Set<GlobalKey> keys = HashSet<GlobalKey>();
for (final Element element in _debugElementsThatWillNeedToBeRebuiltDueToGlobalKeyShenanigans!.keys) {
if (element._lifecycleState != _ElementLifecycle.defunct) {
@ -3601,41 +3512,6 @@ abstract class Element extends DiagnosticableTree implements BuildContext {
BuildOwner? get owner => _owner;
BuildOwner? _owner;
/// A [BuildScope] whose dirty [Element]s can only be rebuilt by
/// [BuildOwner.buildScope] calls whose `context` argument is an [Element]
/// within this [BuildScope].
///
/// The getter typically is only safe to access when this [Element] is [mounted].
///
/// The default implementation returns the parent [Element]'s [buildScope],
/// as in most cases an [Element] is ready to rebuild as soon as its ancestors
/// are no longer dirty. One notable exception is [LayoutBuilder]'s
/// descendants, which must not rebuild until the incoming constraints become
/// available. [LayoutBuilder]'s [Element] overrides [buildScope] to make none
/// of its descendants can rebuild until the incoming constraints are known.
///
/// If you choose to override this getter to establish your own [BuildScope],
/// to flush the dirty [Element]s in the [BuildScope] you need to manually call
/// [BuildOwner.buildScope] with the root [Element] of your [BuildScope] when
/// appropriate, as the Flutter framework does not try to register or manage
/// custom [BuildScope]s.
///
/// Always return the same [BuildScope] instance if you override this getter.
/// Changing the value returned by this getter at runtime is not
/// supported.
///
/// The [updateChild] method ignores [buildScope]: if the parent [Element]
/// calls [updateChild] on a child with a different [BuildScope], the child may
/// still rebuild.
///
/// See also:
///
/// * [LayoutBuilder], a widget that establishes a custom [BuildScope].
BuildScope get buildScope => _parentBuildScope!;
// The cached value of the parent Element's build scope. The cache is updated
// when this Element mounts or reparents.
BuildScope? _parentBuildScope;
/// {@template flutter.widgets.Element.reassemble}
/// Called whenever the application is reassembled during debugging, for
/// example during hot reload.
@ -3672,12 +3548,15 @@ abstract class Element extends DiagnosticableTree implements BuildContext {
});
}
bool _debugIsDescsendantOf(Element target) {
Element? element = this;
while (element != null && element.depth > target.depth) {
element = element._parent;
bool _debugIsInScope(Element target) {
Element? current = this;
while (current != null) {
if (target == current) {
return true;
}
current = current._parent;
}
return element == target;
return false;
}
/// The render object at (or below) this location in the tree.
@ -4223,7 +4102,6 @@ abstract class Element extends DiagnosticableTree implements BuildContext {
// (the root node), the owner should have already been assigned.
// See RootRenderObjectElement.assignOwner().
_owner = parent.owner;
_parentBuildScope = parent.buildScope;
}
assert(owner != null);
final Key? key = widget.key;
@ -4308,19 +4186,6 @@ abstract class Element extends DiagnosticableTree implements BuildContext {
}
}
void _updateBuildScopeRecursively() {
if (identical(buildScope, _parent?.buildScope)) {
return;
}
// Unset the _inDirtyList flag so this Element can be added to the dirty list
// of the new build scope if it's dirty.
_inDirtyList = false;
_parentBuildScope = _parent?.buildScope;
visitChildren((Element child) {
child._updateBuildScopeRecursively();
});
}
/// Remove [renderObject] from the render tree.
///
/// The default implementation of this function calls
@ -4560,7 +4425,6 @@ abstract class Element extends DiagnosticableTree implements BuildContext {
void _activateWithParent(Element parent, Object? newSlot) {
assert(_lifecycleState == _ElementLifecycle.inactive);
_parent = parent;
_owner = parent.owner;
assert(() {
if (debugPrintGlobalKeyedWidgetLifecycle) {
debugPrint('Reactivating $this (now child of $_parent).');
@ -4568,7 +4432,6 @@ abstract class Element extends DiagnosticableTree implements BuildContext {
return true;
}());
_updateDepth(_parent!.depth);
_updateBuildScopeRecursively();
_activateRecursively(this);
attachRenderObject(newSlot);
assert(_lifecycleState == _ElementLifecycle.active);
@ -5131,8 +4994,8 @@ abstract class Element extends DiagnosticableTree implements BuildContext {
bool get dirty => _dirty;
bool _dirty = true;
// Whether this is in _buildScope._dirtyElements. This is used to know whether
// we should be adding the element back into the list when it's reactivated.
// Whether this is in owner._dirtyElements. This is used to know whether we
// should be adding the element back into the list when it's reactivated.
bool _inDirtyList = false;
// Whether we've already built or not. Set in [rebuild].
@ -5156,7 +5019,7 @@ abstract class Element extends DiagnosticableTree implements BuildContext {
if (owner!._debugBuilding) {
assert(owner!._debugCurrentBuildTarget != null);
assert(owner!._debugStateLocked);
if (_debugIsDescsendantOf(owner!._debugCurrentBuildTarget!)) {
if (_debugIsInScope(owner!._debugCurrentBuildTarget!)) {
return true;
}
final List<DiagnosticsNode> information = <DiagnosticsNode>[
@ -6824,7 +6687,6 @@ mixin RootElementMixin on Element {
// ignore: use_setters_to_change_properties, (API predates enforcing the lint)
void assignOwner(BuildOwner owner) {
_owner = owner;
_parentBuildScope = BuildScope();
}
@override

View File

@ -80,11 +80,6 @@ class _LayoutBuilderElement<ConstraintType extends Constraints> extends RenderOb
Element? _child;
@override
BuildScope get buildScope => LayoutBuilder.applyDoubleRebuildFix ? _buildScope : super.buildScope;
late final BuildScope _buildScope = BuildScope(scheduleRebuild: renderObject.markNeedsLayout);
@override
void visitChildren(ElementVisitor visitor) {
if (_child != null) {
@ -102,7 +97,7 @@ class _LayoutBuilderElement<ConstraintType extends Constraints> extends RenderOb
@override
void mount(Element? parent, Object? newSlot) {
super.mount(parent, newSlot); // Creates the renderObject.
renderObject.updateCallback(_rebuildWithConstraints);
renderObject.updateCallback(_layout);
}
@override
@ -112,20 +107,12 @@ class _LayoutBuilderElement<ConstraintType extends Constraints> extends RenderOb
super.update(newWidget);
assert(widget == newWidget);
renderObject.updateCallback(_rebuildWithConstraints);
renderObject.updateCallback(_layout);
if (newWidget.updateShouldRebuild(oldWidget)) {
_needsBuild = true;
renderObject.markNeedsLayout();
renderObject.markNeedsBuild();
}
}
@override
void markNeedsBuild() {
super.markNeedsBuild();
renderObject.markNeedsLayout();
_needsBuild = true;
}
@override
void performRebuild() {
// This gets called if markNeedsBuild() is called on us.
@ -134,8 +121,7 @@ class _LayoutBuilderElement<ConstraintType extends Constraints> extends RenderOb
// Force the callback to be called, even if the layout constraints are the
// same. This is because that callback may depend on the updated widget
// configuration, or an inherited widget.
renderObject.markNeedsLayout();
_needsBuild = true;
renderObject.markNeedsBuild();
super.performRebuild(); // Calls widget.updateRenderObject (a no-op in this case).
}
@ -145,15 +131,9 @@ class _LayoutBuilderElement<ConstraintType extends Constraints> extends RenderOb
super.unmount();
}
// The constraints that were passed to this class last time it was laid out.
// These constraints are compared to the new constraints to determine whether
// [ConstrainedLayoutBuilder.builder] needs to be called.
ConstraintType? _previousConstraints;
bool _needsBuild = true;
void _rebuildWithConstraints(ConstraintType constraints) {
void _layout(ConstraintType constraints) {
@pragma('vm:notify-debugger-on-exception')
void updateChildCallback() {
void layoutCallback() {
Widget built;
try {
built = (widget as ConstrainedLayoutBuilder<ConstraintType>).builder(this, constraints);
@ -187,16 +167,10 @@ class _LayoutBuilderElement<ConstraintType extends Constraints> extends RenderOb
),
);
_child = updateChild(null, built, slot);
} finally {
_needsBuild = false;
_previousConstraints = constraints;
}
}
final VoidCallback? callback = _needsBuild || (constraints != _previousConstraints)
? updateChildCallback
: null;
owner!.buildScope(this, callback);
owner!.buildScope(this, layoutCallback);
}
@override
@ -237,13 +211,42 @@ mixin RenderConstrainedLayoutBuilder<ConstraintType extends Constraints, ChildTy
markNeedsLayout();
}
bool _needsBuild = true;
/// Marks this layout builder as needing to rebuild.
///
/// The layout build rebuilds automatically when layout constraints change.
/// However, we must also rebuild when the widget updates, e.g. after
/// [State.setState], or [State.didChangeDependencies], even when the layout
/// constraints remain unchanged.
///
/// See also:
///
/// * [ConstrainedLayoutBuilder.builder], which is called during the rebuild.
void markNeedsBuild() {
// Do not call the callback directly. It must be called during the layout
// phase, when parent constraints are available. Calling `markNeedsLayout`
// will cause it to be called at the right time.
_needsBuild = true;
markNeedsLayout();
}
// The constraints that were passed to this class last time it was laid out.
// These constraints are compared to the new constraints to determine whether
// [ConstrainedLayoutBuilder.builder] needs to be called.
Constraints? _previousConstraints;
/// Invoke the callback supplied via [updateCallback].
///
/// Typically this results in [ConstrainedLayoutBuilder.builder] being called
/// during layout.
void rebuildIfNecessary() {
assert(_callback != null);
invokeLayoutCallback(_callback!);
if (_needsBuild || constraints != _previousConstraints) {
_previousConstraints = constraints;
_needsBuild = false;
invokeLayoutCallback(_callback!);
}
}
}
@ -284,12 +287,6 @@ class LayoutBuilder extends ConstrainedLayoutBuilder<BoxConstraints> {
required super.builder,
});
/// Temporary flag that controls whether [LayoutBuilder]s and
/// [SliverLayoutBuilder]s should apply the double rebuild fix. This flag is
/// for migration only and **SHOULD NOT BE USED**.
@Deprecated('This is a temporary migration flag. DO NOT USE THIS.') // flutter_ignore: deprecation_syntax (see analyze.dart)
static bool applyDoubleRebuildFix = false;
@override
RenderObject createRenderObject(BuildContext context) => _RenderLayoutBuilder();
}

View File

@ -23,9 +23,6 @@ class _MyGlobalObjectKey<T extends State<StatefulWidget>> extends GlobalObjectKe
}
void main() {
setUp(() { LayoutBuilder.applyDoubleRebuildFix = true; });
tearDown(() { LayoutBuilder.applyDoubleRebuildFix = false; });
testWidgets('UniqueKey control test', (WidgetTester tester) async {
final Key key = UniqueKey();
expect(key, hasOneLineDescription);
@ -140,8 +137,8 @@ void main() {
});
testWidgets('GlobalKey correct case 3 - can deal with early rebuild in layoutbuilder - move backward', (WidgetTester tester) async {
final Key key1 = GlobalKey(debugLabel: 'Text1');
final Key key2 = GlobalKey(debugLabel: 'Text2');
const Key key1 = GlobalObjectKey('Text1');
const Key key2 = GlobalObjectKey('Text2');
Key? rebuiltKeyOfSecondChildBeforeLayout;
Key? rebuiltKeyOfFirstChildAfterLayout;
Key? rebuiltKeyOfSecondChildAfterLayout;
@ -150,7 +147,7 @@ void main() {
builder: (BuildContext context, BoxConstraints constraints) {
return Column(
children: <Widget>[
_Stateful(
const _Stateful(
child: Text(
'Text1',
textDirection: TextDirection.ltr,
@ -158,7 +155,7 @@ void main() {
),
),
_Stateful(
child: Text(
child: const Text(
'Text2',
textDirection: TextDirection.ltr,
key: key2,
@ -189,14 +186,14 @@ void main() {
return Column(
children: <Widget>[
_Stateful(
child: Text(
child: const Text(
'Text2',
textDirection: TextDirection.ltr,
key: key2,
),
onElementRebuild: (StatefulElement element) {
// The widget is only built once.
expect(rebuiltKeyOfSecondChildBeforeLayout, isNull);
// Verifies the early rebuild happens before layout.
expect(rebuiltKeyOfSecondChildBeforeLayout, key2);
// We don't want noise to override the result;
expect(rebuiltKeyOfFirstChildAfterLayout, isNull);
final _Stateful statefulWidget = element.widget as _Stateful;
@ -204,14 +201,14 @@ void main() {
},
),
_Stateful(
child: Text(
child: const Text(
'Text1',
textDirection: TextDirection.ltr,
key: key1,
),
onElementRebuild: (StatefulElement element) {
// The widget is only built once.
expect(rebuiltKeyOfSecondChildBeforeLayout, isNull);
// Verifies the early rebuild happens before layout.
expect(rebuiltKeyOfSecondChildBeforeLayout, key2);
// We don't want noise to override the result;
expect(rebuiltKeyOfSecondChildAfterLayout, isNull);
final _Stateful statefulWidget = element.widget as _Stateful;
@ -223,7 +220,7 @@ void main() {
},
),
);
expect(rebuiltKeyOfSecondChildBeforeLayout, isNull);
expect(rebuiltKeyOfSecondChildBeforeLayout, key2);
expect(rebuiltKeyOfFirstChildAfterLayout, key2);
expect(rebuiltKeyOfSecondChildAfterLayout, key1);
});
@ -298,8 +295,8 @@ void main() {
key: key3,
),
onElementRebuild: (StatefulElement element) {
// The widget is only built once.
expect(rebuiltKeyOfSecondChildBeforeLayout, isNull);
// Verifies the early rebuild happens before layout.
expect(rebuiltKeyOfSecondChildBeforeLayout, key2);
// We don't want noise to override the result;
expect(rebuiltKeyOfSecondChildAfterLayout, isNull);
final _Stateful statefulWidget = element.widget as _Stateful;
@ -313,8 +310,8 @@ void main() {
key: key2,
),
onElementRebuild: (StatefulElement element) {
// The widget is only built once.
expect(rebuiltKeyOfSecondChildBeforeLayout, isNull);
// Verifies the early rebuild happens before layout.
expect(rebuiltKeyOfSecondChildBeforeLayout, key2);
// We don't want noise to override the result;
expect(rebuiltKeyOfThirdChildAfterLayout, isNull);
final _Stateful statefulWidget = element.widget as _Stateful;
@ -326,7 +323,7 @@ void main() {
},
),
);
expect(rebuiltKeyOfSecondChildBeforeLayout, isNull);
expect(rebuiltKeyOfSecondChildBeforeLayout, key2);
expect(rebuiltKeyOfSecondChildAfterLayout, key3);
expect(rebuiltKeyOfThirdChildAfterLayout, key2);
});
@ -394,8 +391,8 @@ void main() {
textDirection: TextDirection.ltr,
),
onElementRebuild: (StatefulElement element) {
// The widget is only built once.
expect(rebuiltKeyOfSecondChildBeforeLayout, isNull);
// Verifies the early rebuild happens before layout.
expect(rebuiltKeyOfSecondChildBeforeLayout, key1);
},
),
_Stateful(
@ -405,8 +402,8 @@ void main() {
key: key1,
),
onElementRebuild: (StatefulElement element) {
// The widget is only built once.
expect(rebuiltKeyOfSecondChildBeforeLayout, isNull);
// Verifies the early rebuild happens before layout.
expect(rebuiltKeyOfSecondChildBeforeLayout, key1);
// We don't want noise to override the result;
expect(rebuiltKeyOfThirdChildAfterLayout, isNull);
final _Stateful statefulWidget = element.widget as _Stateful;
@ -418,7 +415,7 @@ void main() {
},
),
);
expect(rebuiltKeyOfSecondChildBeforeLayout, isNull);
expect(rebuiltKeyOfSecondChildBeforeLayout, key1);
expect(rebuiltKeyOfThirdChildAfterLayout, key1);
});
@ -993,8 +990,8 @@ void main() {
key: key2,
),
onElementRebuild: (StatefulElement element) {
// The widget is only rebuilt once.
expect(rebuiltKeyOfSecondChildBeforeLayout, isNull);
// Verifies the early rebuild happens before layout.
expect(rebuiltKeyOfSecondChildBeforeLayout, key2);
// We don't want noise to override the result;
expect(rebuiltKeyOfFirstChildAfterLayout, isNull);
final _Stateful statefulWidget = element.widget as _Stateful;
@ -1008,8 +1005,8 @@ void main() {
key: key2,
),
onElementRebuild: (StatefulElement element) {
// The widget is only rebuilt once.
expect(rebuiltKeyOfSecondChildBeforeLayout, isNull);
// Verifies the early rebuild happens before layout.
expect(rebuiltKeyOfSecondChildBeforeLayout, key2);
// We don't want noise to override the result;
expect(rebuiltKeyOfSecondChildAfterLayout, isNull);
final _Stateful statefulWidget = element.widget as _Stateful;
@ -1021,7 +1018,7 @@ void main() {
},
),
);
expect(rebuiltKeyOfSecondChildBeforeLayout, isNull);
expect(rebuiltKeyOfSecondChildBeforeLayout, key2);
expect(rebuiltKeyOfFirstChildAfterLayout, key2);
expect(rebuiltKeyOfSecondChildAfterLayout, key2);
final dynamic exception = tester.takeException();
@ -1294,7 +1291,7 @@ void main() {
late FlutterError error;
try {
tester.binding.buildOwner!.scheduleBuildFor(
DirtyElementWithCustomBuildOwner(tester.binding.buildOwner!, Container())
DirtyElementWithCustomBuildOwner(tester.binding.buildOwner!, Container()),
);
} on FlutterError catch (e) {
error = e;
@ -1855,95 +1852,6 @@ The findRenderObject() method was called for the following element:
));
expect(tester.takeException(), isNull);
});
testWidgets('BuildScope segregates dirty elements', (WidgetTester tester) async {
final BuildScope buildScope = BuildScope();
await tester.pumpWidget(
StatefulBuilder(
builder: (BuildContext context, StateSetter stateSetter) {
return _CustomBuildScopeWidget(
buildScope: buildScope,
child: const _NullLeaf(),
);
},
),
);
final Element rootElement = tester.element(find.byType(StatefulBuilder));
final Element scopeElement = tester.element(find.byType(_CustomBuildScopeWidget));
final Element leafElement = tester.element(find.byType(_NullLeaf));
expect(rootElement.dirty, isFalse);
expect(scopeElement.dirty, isFalse);
expect(leafElement.dirty, isFalse);
rootElement.markNeedsBuild();
await tester.pump();
expect(rootElement.dirty, isFalse);
expect(scopeElement.dirty, isFalse);
expect(leafElement.dirty, isFalse);
scopeElement.markNeedsBuild();
await tester.pump();
expect(rootElement.dirty, isFalse);
expect(scopeElement.dirty, isTrue);
expect(leafElement.dirty, isFalse);
scopeElement.owner!.buildScope(scopeElement);
await tester.pump();
expect(rootElement.dirty, isFalse);
expect(scopeElement.dirty, isFalse);
expect(leafElement.dirty, isFalse);
leafElement.markNeedsBuild();
await tester.pump();
expect(rootElement.dirty, isFalse);
expect(scopeElement.dirty, isFalse);
expect(leafElement.dirty, isTrue);
scopeElement.owner!.buildScope(scopeElement);
await tester.pump();
expect(rootElement.dirty, isFalse);
expect(scopeElement.dirty, isFalse);
expect(leafElement.dirty, isFalse);
});
testWidgets('reparenting Element to another BuildScope', (WidgetTester tester) async {
final BuildScope buildScope = BuildScope();
final GlobalKey key = GlobalKey(debugLabel: 'key');
await tester.pumpWidget(
_DummyMultiChildWidget(
<Widget>[
_CustomBuildScopeWidget( // This widget does not call updateChild when rebuild.
buildScope: buildScope,
child: const _NullLeaf(),
),
_NullLeaf(key: key),
],
),
);
final Element scopeElement = tester.element(find.byType(_CustomBuildScopeWidget));
final Element keyedWidget = tester.element(find.byKey(key));
expect(scopeElement.dirty, isFalse);
expect(keyedWidget.dirty, isFalse);
// Mark the Element with a GlobalKey dirty and reparent it to another BuildScope.
// the Element should not rebuild.
keyedWidget.markNeedsBuild();
await tester.pumpWidget(
_DummyMultiChildWidget(
<Widget>[
_CustomBuildScopeWidget( // This widget does not call updateChild when rebuild.
buildScope: buildScope,
child: _NullLeaf(key: key),
),
const _NullLeaf(),
],
),
);
expect(scopeElement.dirty, isFalse);
expect(keyedWidget.dirty, isTrue);
});
}
class _TestInheritedElement extends InheritedElement {
@ -2043,10 +1951,14 @@ class _DecorateState extends State<Decorate> {
}
}
class DirtyElementWithCustomBuildOwner extends Element with RootElementMixin {
DirtyElementWithCustomBuildOwner(BuildOwner buildOwner, super.widget) {
assignOwner(buildOwner);
}
class DirtyElementWithCustomBuildOwner extends Element {
DirtyElementWithCustomBuildOwner(BuildOwner buildOwner, super.widget)
: _owner = buildOwner;
final BuildOwner _owner;
@override
BuildOwner get owner => _owner;
@override
bool get dirty => true;
@ -2170,9 +2082,9 @@ class StatefulElementSpy extends StatefulElement {
_Stateful get _statefulWidget => widget as _Stateful;
@override
void performRebuild() {
void rebuild({bool force = false}) {
_statefulWidget.onElementRebuild?.call(this);
super.performRebuild();
super.rebuild(force: force);
}
}
@ -2376,116 +2288,3 @@ class _RenderTestLeaderLayerWidget extends RenderProxyBox {
}
}
}
// This widget does not call updateChild when it rebuilds.
class _CustomBuildScopeWidget extends ProxyWidget {
const _CustomBuildScopeWidget({this.buildScope, required super.child});
final BuildScope? buildScope;
@override
Element createElement() => _CustomBuildScopeElement(this);
}
class _CustomBuildScopeElement extends Element {
_CustomBuildScopeElement(super.widget);
@override
BuildScope get buildScope => (widget as _CustomBuildScopeWidget).buildScope ?? super.buildScope;
@override
bool get debugDoingBuild => throw UnimplementedError();
Element? _child;
@override
void mount(Element? parent, Object? newSlot) {
super.mount(parent, newSlot);
rebuild(force: true);
// Only does tree walk on mount.
_child = updateChild(_child, (widget as _CustomBuildScopeWidget).child, slot);
}
@override
void visitChildren(ElementVisitor visitor) {
final Element? child = _child;
if (child != null) {
visitor(child);
}
}
}
class _DummyMultiChildWidget extends Widget {
const _DummyMultiChildWidget(this.children);
final List<Widget> children;
@override
Element createElement() => _DummyMuitiChildElement(this);
}
class _DummyMuitiChildElement extends Element {
_DummyMuitiChildElement(super.widget);
@override
bool get debugDoingBuild => throw UnimplementedError();
late List<Element> _children;
final Set<Element> _forgottenChildren = <Element>{};
@override
void mount(Element? parent, Object? newSlot) {
super.mount(parent, newSlot);
final List<Widget> childWidgets = (widget as _DummyMultiChildWidget).children;
Element? previousChild;
final List<Element> children = List<Element>.generate(childWidgets.length, (int i) {
final Element child = previousChild = inflateWidget(childWidgets[i], IndexedSlot<Element?>(i, previousChild));
return child;
});
_children = children;
}
@override
void update(_DummyMultiChildWidget newWidget) {
super.update(newWidget);
_children = updateChildren(_children, newWidget.children, forgottenChildren: _forgottenChildren);
_forgottenChildren.clear();
}
@override
Element? get renderObjectAttachingChild => null;
@override
void visitChildren(ElementVisitor visitor) {
for (final Element child in _children) {
if (!_forgottenChildren.contains(child)) {
visitor(child);
}
}
}
@override
void forgetChild(Element child) {
assert(_children.contains(child));
assert(!_forgottenChildren.contains(child));
_forgottenChildren.add(child);
super.forgetChild(child);
}
}
class _NullLeaf extends Widget {
const _NullLeaf({super.key});
@override
Element createElement() => _NullElement(this);
}
class _NullElement extends Element {
_NullElement(super.widget);
@override
void mount(Element? parent, Object? newSlot) {
super.mount(parent, newSlot);
rebuild(force: true);
}
@override
bool get debugDoingBuild => throw UnimplementedError();
}

View File

@ -41,9 +41,6 @@ class StatefulWrapperState extends State<StatefulWrapper> {
}
void main() {
setUp(() { LayoutBuilder.applyDoubleRebuildFix = true; });
tearDown(() { LayoutBuilder.applyDoubleRebuildFix = false; });
testWidgets('Moving global key inside a LayoutBuilder', (WidgetTester tester) async {
final GlobalKey<StatefulWrapperState> key = GlobalKey<StatefulWrapperState>();
await tester.pumpWidget(
@ -63,45 +60,6 @@ void main() {
expect(tester.takeException(), null);
});
testWidgets('Moving GlobalKeys out of LayoutBuilder', (WidgetTester tester) async {
// Regression test for https://github.com/flutter/flutter/issues/146379.
final GlobalKey widgetKey = GlobalKey(debugLabel: 'widget key');
final Widget widgetWithKey = Builder(builder: (BuildContext context) {
Directionality.of(context);
return SizedBox(key: widgetKey);
});
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: Row(
children: <Widget>[
LayoutBuilder(
builder: (BuildContext context, BoxConstraints constraints) => widgetWithKey,
),
],
),
),
);
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.rtl,
child: Row(
children: <Widget>[
LayoutBuilder(
builder: (BuildContext context, BoxConstraints constraints) => const Placeholder(),
),
widgetWithKey,
],
),
),
);
expect(tester.takeException(), null);
expect(find.byKey(widgetKey), findsOneWidget);
});
testWidgets('Moving global key inside a SliverLayoutBuilder', (WidgetTester tester) async {
final GlobalKey<StatefulWrapperState> key = GlobalKey<StatefulWrapperState>();

View File

@ -50,6 +50,7 @@ class Wrapper extends StatelessWidget {
void main() {
testWidgets('Calling setState on a widget that moves into a LayoutBuilder in the same frame', (WidgetTester tester) async {
StatefulWrapperState statefulWrapper;
final Widget inner = Wrapper(
child: StatefulWrapper(
key: GlobalKey(),
@ -62,7 +63,7 @@ void main() {
}),
right: inner,
));
final StatefulWrapperState statefulWrapper = tester.state(find.byType(StatefulWrapper));
statefulWrapper = tester.state(find.byType(StatefulWrapper));
expect(statefulWrapper.built, true);
statefulWrapper.built = false;

View File

@ -7,9 +7,6 @@ import 'package:flutter/widgets.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
setUp(() { LayoutBuilder.applyDoubleRebuildFix = true; });
tearDown(() { LayoutBuilder.applyDoubleRebuildFix = false; });
testWidgets('LayoutBuilder parent size', (WidgetTester tester) async {
late Size layoutBuilderSize;
final Key childKey = UniqueKey();
@ -297,84 +294,6 @@ void main() {
expect(built, 2);
});
testWidgets('LayoutBuilder rebuilds once in the same frame', (WidgetTester tester) async {
// Regression test for https://github.com/flutter/flutter/issues/146379.
int built = 0;
final Widget target = LayoutBuilder(
builder: (BuildContext context, BoxConstraints constraints) {
return Builder(builder: (BuildContext context) {
built += 1;
MediaQuery.of(context);
return const Placeholder();
});
},
);
expect(built, 0);
await tester.pumpWidget(MediaQuery(
data: const MediaQueryData(size: Size(400.0, 300.0)),
child: Center(
child: SizedBox(
width: 400.0,
child: target,
),
),
));
expect(built, 1);
await tester.pumpWidget(MediaQuery(
data: const MediaQueryData(size: Size(300.0, 400.0)),
child: Center(
child: SizedBox(
width: 300.0,
child: target,
),
),
));
expect(built, 2);
});
testWidgets('LayoutBuilder can change size without rebuild', (WidgetTester tester) async {
int built = 0;
final Widget target = LayoutBuilder(
builder: (BuildContext context, BoxConstraints constraints) {
return Builder(builder: (BuildContext context) {
built += 1;
return const Text('A');
});
},
);
expect(built, 0);
await tester.pumpWidget(
Center(
child: Directionality(
textDirection: TextDirection.ltr,
child: DefaultTextStyle(
style: const TextStyle(fontSize: 10),
child: target,
),
),
)
);
expect(built, 1);
expect(tester.getSize(find.byWidget(target)), const Size(10, 10));
await tester.pumpWidget(
Center(
child: Directionality(
textDirection: TextDirection.ltr,
child: DefaultTextStyle(
style: const TextStyle(fontSize: 100),
child: target,
),
),
)
);
expect(built, 1);
expect(tester.getSize(find.byWidget(target)), const Size(100, 100));
});
testWidgets('SliverLayoutBuilder and Inherited -- do not rebuild when not using inherited', (WidgetTester tester) async {
int built = 0;
final Widget target = Directionality(

View File

@ -29,7 +29,7 @@ class _ManyRelayoutBoundaries extends StatelessWidget {
}
}
void rebuildLayoutBuilderSubtree(RenderBox descendant, WidgetTester tester) {
void rebuildLayoutBuilderSubtree(RenderBox descendant) {
assert(descendant is! RenderConstrainedLayoutBuilder<BoxConstraints, RenderBox>);
RenderObject? node = descendant.parent;
@ -37,10 +37,7 @@ void rebuildLayoutBuilderSubtree(RenderBox descendant, WidgetTester tester) {
if (node is! RenderConstrainedLayoutBuilder<BoxConstraints, RenderBox>) {
node = node.parent;
} else {
final Element layoutBuilderElement = tester.element(find.byElementPredicate(
(Element element) => element.widget is LayoutBuilder && element.renderObject == node,
));
layoutBuilderElement.markNeedsBuild();
node.markNeedsBuild();
return;
}
}
@ -714,7 +711,7 @@ void main() {
renderChild1.markNeedsLayout();
// Dirty both render subtree branches.
childBox.markNeedsLayout();
rebuildLayoutBuilderSubtree(overlayChildBox, tester);
rebuildLayoutBuilderSubtree(overlayChildBox);
// Make sure childBox's depth is greater than that of the overlay
// child, and childBox's parent isn't dirty (childBox is a dirty relayout
@ -1113,7 +1110,7 @@ void main() {
widgetKey.currentContext!.findRenderObject()!.markNeedsLayout();
childBox.markNeedsLayout();
rebuildLayoutBuilderSubtree(overlayChildBox, tester);
rebuildLayoutBuilderSubtree(overlayChildBox);
// Make sure childBox's depth is greater than that of the overlay child.
expect(
widgetKey.currentContext!.findRenderObject()!.depth,
@ -1193,7 +1190,7 @@ void main() {
targetGlobalKey.currentContext!.findRenderObject()!.markNeedsLayout();
childBox.markNeedsLayout();
rebuildLayoutBuilderSubtree(overlayChildBox, tester);
rebuildLayoutBuilderSubtree(overlayChildBox);
setState1(() {});
setState2(() {});
targetMovedToOverlayEntry3 = true;