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:
parent
182c1e6a36
commit
ebc414337d
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
|
@ -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();
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
|
@ -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>();
|
||||
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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(
|
||||
|
@ -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;
|
||||
|
Loading…
x
Reference in New Issue
Block a user