Skip to content

Commit 67d4a83

Browse files
authored
Allow multiple ParentDataWidgets to write to ParentData (flutter#133581)
Fixes flutter#133089 This allows more than one ParentDataWidget to write to the ParentData of a child render object. Previously only one was allowed. There are some rules though: 1. Only one of a given type of `ParentDataWidget` can write to the `ParentData` of a given child. a. For example, 2 `Positioned` widgets wrapping a child of a `Stack` would not be allowed, as only one of type `Positioned` can contribute data. 2. The type of `ParentData` **must** be compatible with all of the `ParentDataWidget`s that want to contribute data. a. For example, `TwoDimensionalViewportParentData` mixes in the `KeepAliveParentDataMixin`. So the `ParentData` of a given child would be compatible with the `KeepAlive` `ParentDataWidget`, as well as another `ParentDataWidget` that writes `TwoDimensionalViewportParentData` (or a subclass of `TwoDimensionalViewportParentData` - This was the motivation for this change, where a `ParentDataWidget` is being used in `TableView` with the parent data type being a subclass of `TwoDimensionalViewportParentData`.)
1 parent d81c8aa commit 67d4a83

File tree

4 files changed

+424
-45
lines changed

4 files changed

+424
-45
lines changed

packages/flutter/lib/src/widgets/framework.dart

Lines changed: 125 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -5824,12 +5824,28 @@ class ParentDataElement<T extends ParentData> extends ProxyElement {
58245824
/// Creates an element that uses the given widget as its configuration.
58255825
ParentDataElement(ParentDataWidget<T> super.widget);
58265826

5827+
/// Returns the [Type] of [ParentData] that this element has been configured
5828+
/// for.
5829+
///
5830+
/// This is only available in debug mode. It will throw in profile and
5831+
/// release modes.
5832+
Type get debugParentDataType {
5833+
Type? type;
5834+
assert(() {
5835+
type = T;
5836+
return true;
5837+
}());
5838+
if (type != null) {
5839+
return type!;
5840+
}
5841+
throw UnsupportedError('debugParentDataType is only supported in debug builds');
5842+
}
5843+
58275844
void _applyParentData(ParentDataWidget<T> widget) {
58285845
void applyParentDataToChild(Element child) {
58295846
if (child is RenderObjectElement) {
58305847
child._updateParentData(widget);
58315848
} else {
5832-
assert(child is! ParentDataElement<ParentData>);
58335849
child.visitChildren(applyParentDataToChild);
58345850
}
58355851
}
@@ -6278,50 +6294,124 @@ abstract class RenderObjectElement extends Element {
62786294
return ancestor as RenderObjectElement?;
62796295
}
62806296

6281-
ParentDataElement<ParentData>? _findAncestorParentDataElement() {
6282-
Element? ancestor = _parent;
6283-
ParentDataElement<ParentData>? result;
6284-
while (ancestor != null && ancestor is! RenderObjectElement) {
6285-
if (ancestor is ParentDataElement<ParentData>) {
6286-
result = ancestor;
6287-
break;
6288-
}
6289-
ancestor = ancestor._parent;
6290-
}
6297+
void _debugCheckCompetingAncestors(
6298+
List<ParentDataElement<ParentData>> result,
6299+
Set<Type> debugAncestorTypes,
6300+
Set<Type> debugParentDataTypes,
6301+
List<Type> debugAncestorCulprits,
6302+
) {
62916303
assert(() {
6292-
if (result == null || ancestor == null) {
6293-
return true;
6294-
}
6295-
// Check that no other ParentDataWidgets want to provide parent data.
6296-
final List<ParentDataElement<ParentData>> badAncestors = <ParentDataElement<ParentData>>[];
6297-
ancestor = ancestor!._parent;
6298-
while (ancestor != null && ancestor is! RenderObjectElement) {
6299-
if (ancestor is ParentDataElement<ParentData>) {
6300-
badAncestors.add(ancestor! as ParentDataElement<ParentData>);
6301-
}
6302-
ancestor = ancestor!._parent;
6303-
}
6304-
if (badAncestors.isNotEmpty) {
6305-
badAncestors.insert(0, result);
6304+
// Check that no other ParentDataWidgets of the same
6305+
// type want to provide parent data.
6306+
if (debugAncestorTypes.length != result.length || debugParentDataTypes.length != result.length) {
6307+
// This can only occur if the Sets of ancestors and parent data types was
6308+
// provided a dupe and did not add it.
6309+
assert(debugAncestorTypes.length < result.length || debugParentDataTypes.length < result.length);
63066310
try {
63076311
// We explicitly throw here (even though we immediately redirect the
63086312
// exception elsewhere) so that debuggers will notice it when they
63096313
// have "break on exception" enabled.
63106314
throw FlutterError.fromParts(<DiagnosticsNode>[
63116315
ErrorSummary('Incorrect use of ParentDataWidget.'),
6312-
ErrorDescription('The following ParentDataWidgets are providing parent data to the same RenderObject:'),
6313-
for (final ParentDataElement<ParentData> ancestor in badAncestors)
6314-
ErrorDescription('- ${ancestor.widget} (typically placed directly inside a ${(ancestor.widget as ParentDataWidget<ParentData>).debugTypicalAncestorWidgetDescription} widget)'),
6315-
ErrorDescription('However, a RenderObject can only receive parent data from at most one ParentDataWidget.'),
6316-
ErrorHint('Usually, this indicates that at least one of the offending ParentDataWidgets listed above is not placed directly inside a compatible ancestor widget.'),
6317-
ErrorDescription('The ownership chain for the RenderObject that received the parent data was:\n ${debugGetCreatorChain(10)}'),
6316+
ErrorDescription(
6317+
'Competing ParentDataWidgets are providing parent data to the '
6318+
'same RenderObject:'
6319+
),
6320+
for (final ParentDataElement<ParentData> ancestor in result.where((ParentDataElement<ParentData> ancestor) {
6321+
return debugAncestorCulprits.contains(ancestor.runtimeType);
6322+
}))
6323+
ErrorDescription(
6324+
'- ${ancestor.widget}, which writes ParentData of type '
6325+
'${ancestor.debugParentDataType}, (typically placed directly '
6326+
'inside a '
6327+
'${(ancestor.widget as ParentDataWidget<ParentData>).debugTypicalAncestorWidgetClass} '
6328+
'widget)'
6329+
),
6330+
ErrorDescription(
6331+
'A RenderObject can receive parent data from multiple '
6332+
'ParentDataWidgets, but the Type of ParentData must be unique to '
6333+
'prevent one overwriting another.'
6334+
),
6335+
ErrorHint(
6336+
'Usually, this indicates that one or more of the offending '
6337+
"ParentDataWidgets listed above isn't placed inside a dedicated "
6338+
"compatible ancestor widget that it isn't sharing with another "
6339+
'ParentDataWidget of the same type.'
6340+
),
6341+
ErrorHint(
6342+
'Otherwise, separating aspects of ParentData to prevent '
6343+
'conflicts can be done using mixins, mixing them all in on the '
6344+
'full ParentData Object, such as KeepAlive does with '
6345+
'KeepAliveParentDataMixin.'
6346+
),
6347+
ErrorDescription(
6348+
'The ownership chain for the RenderObject that received the '
6349+
'parent data was:\n ${debugGetCreatorChain(10)}'
6350+
),
63186351
]);
6319-
} on FlutterError catch (e) {
6320-
_reportException(ErrorSummary('while looking for parent data.'), e, e.stackTrace);
6352+
} on FlutterError catch (error) {
6353+
_reportException(
6354+
ErrorSummary('while looking for parent data.'),
6355+
error,
6356+
error.stackTrace,
6357+
);
63216358
}
63226359
}
63236360
return true;
63246361
}());
6362+
}
6363+
6364+
List<ParentDataElement<ParentData>> _findAncestorParentDataElements() {
6365+
Element? ancestor = _parent;
6366+
final List<ParentDataElement<ParentData>> result = <ParentDataElement<ParentData>>[];
6367+
final Set<Type> debugAncestorTypes = <Type>{};
6368+
final Set<Type> debugParentDataTypes = <Type>{};
6369+
final List<Type> debugAncestorCulprits = <Type>[];
6370+
6371+
// More than one ParentDataWidget can contribute ParentData, but there are
6372+
// some constraints.
6373+
// 1. ParentData can only be written by unique ParentDataWidget types.
6374+
// For example, two KeepAlive ParentDataWidgets trying to write to the
6375+
// same child is not allowed.
6376+
// 2. Each contributing ParentDataWidget must contribute to a unique
6377+
// ParentData type, less ParentData be overwritten.
6378+
// For example, there cannot be two ParentDataWidgets that both write
6379+
// ParentData of type KeepAliveParentDataMixin, if the first check was
6380+
// subverted by a subclassing of the KeepAlive ParentDataWidget.
6381+
// 3. The ParentData itself must be compatible with all ParentDataWidgets
6382+
// writing to it.
6383+
// For example, TwoDimensionalViewportParentData uses the
6384+
// KeepAliveParentDataMixin, so it could be compatible with both
6385+
// KeepAlive, and another ParentDataWidget with ParentData type
6386+
// TwoDimensionalViewportParentData or a subclass thereof.
6387+
// The first and second cases are verified here. The third is verified in
6388+
// debugIsValidRenderObject.
6389+
6390+
while (ancestor != null && ancestor is! RenderObjectElement) {
6391+
if (ancestor is ParentDataElement<ParentData>) {
6392+
assert((ParentDataElement<ParentData> ancestor) {
6393+
if (!debugAncestorTypes.add(ancestor.runtimeType) || !debugParentDataTypes.add(ancestor.debugParentDataType)) {
6394+
debugAncestorCulprits.add(ancestor.runtimeType);
6395+
}
6396+
return true;
6397+
}(ancestor));
6398+
result.add(ancestor);
6399+
}
6400+
ancestor = ancestor._parent;
6401+
}
6402+
assert(() {
6403+
if (result.isEmpty || ancestor == null) {
6404+
return true;
6405+
}
6406+
// Validate points 1 and 2 from above.
6407+
_debugCheckCompetingAncestors(
6408+
result,
6409+
debugAncestorTypes,
6410+
debugParentDataTypes,
6411+
debugAncestorCulprits,
6412+
);
6413+
return true;
6414+
}());
63256415
return result;
63266416
}
63276417

@@ -6477,8 +6567,8 @@ abstract class RenderObjectElement extends Element {
64776567
return true;
64786568
}());
64796569
_ancestorRenderObjectElement?.insertRenderObjectChild(renderObject, newSlot);
6480-
final ParentDataElement<ParentData>? parentDataElement = _findAncestorParentDataElement();
6481-
if (parentDataElement != null) {
6570+
final List<ParentDataElement<ParentData>> parentDataElements = _findAncestorParentDataElements();
6571+
for (final ParentDataElement<ParentData> parentDataElement in parentDataElements) {
64826572
_updateParentData(parentDataElement.widget as ParentDataWidget<ParentData>);
64836573
}
64846574
}

packages/flutter/test/widgets/parent_data_test.dart

Lines changed: 143 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -250,12 +250,99 @@ void main() {
250250
checkTree(tester, <TestParentData>[]);
251251
});
252252

253+
testWidgetsWithLeakTracking('ParentData overwrite with custom ParentDataWidget subclasses', (WidgetTester tester) async {
254+
await tester.pumpWidget(
255+
const Directionality(
256+
textDirection: TextDirection.ltr,
257+
child: Stack(
258+
children: <Widget>[
259+
CustomPositionedWidget(
260+
bottom: 8.0,
261+
child: Positioned(
262+
top: 6.0,
263+
left: 7.0,
264+
child: DecoratedBox(decoration: kBoxDecorationB),
265+
),
266+
),
267+
],
268+
),
269+
),
270+
);
271+
272+
dynamic exception = tester.takeException();
273+
expect(exception, isFlutterError);
274+
expect(
275+
exception.toString(),
276+
startsWith(
277+
'Incorrect use of ParentDataWidget.\n'
278+
'Competing ParentDataWidgets are providing parent data to the same RenderObject:\n'
279+
'- Positioned(left: 7.0, top: 6.0), which writes ParentData of type '
280+
'StackParentData, (typically placed directly inside a Stack widget)\n'
281+
'- CustomPositionedWidget, which writes ParentData of type '
282+
'StackParentData, (typically placed directly inside a Stack widget)\n'
283+
'A RenderObject can receive parent data from multiple '
284+
'ParentDataWidgets, but the Type of ParentData must be unique to '
285+
'prevent one overwriting another.\n'
286+
'Usually, this indicates that one or more of the offending ParentDataWidgets listed '
287+
"above isn't placed inside a dedicated compatible ancestor widget that it isn't "
288+
'sharing with another ParentDataWidget of the same type.\n'
289+
'Otherwise, separating aspects of ParentData to prevent conflicts can '
290+
'be done using mixins, mixing them all in on the full ParentData '
291+
'Object, such as KeepAlive does with KeepAliveParentDataMixin.\n'
292+
'The ownership chain for the RenderObject that received the parent data was:\n'
293+
' DecoratedBox ← Positioned ← CustomPositionedWidget ← Stack ← Directionality ← ', // End of chain omitted, not relevant for test.
294+
),
295+
);
296+
297+
await tester.pumpWidget(
298+
const Directionality(
299+
textDirection: TextDirection.ltr,
300+
child: Stack(
301+
children: <Widget>[
302+
SubclassPositioned(
303+
bottom: 8.0,
304+
child: Positioned(
305+
top: 6.0,
306+
left: 7.0,
307+
child: DecoratedBox(decoration: kBoxDecorationB),
308+
),
309+
),
310+
],
311+
),
312+
),
313+
);
314+
315+
exception = tester.takeException();
316+
expect(exception, isFlutterError);
317+
expect(
318+
exception.toString(),
319+
startsWith(
320+
'Incorrect use of ParentDataWidget.\n'
321+
'Competing ParentDataWidgets are providing parent data to the same RenderObject:\n'
322+
'- Positioned(left: 7.0, top: 6.0), which writes ParentData of type '
323+
'StackParentData, (typically placed directly inside a Stack widget)\n'
324+
'- SubclassPositioned(bottom: 8.0), which writes ParentData of type '
325+
'StackParentData, (typically placed directly inside a Stack widget)\n'
326+
'A RenderObject can receive parent data from multiple '
327+
'ParentDataWidgets, but the Type of ParentData must be unique to '
328+
'prevent one overwriting another.\n'
329+
'Usually, this indicates that one or more of the offending ParentDataWidgets listed '
330+
"above isn't placed inside a dedicated compatible ancestor widget that it isn't "
331+
'sharing with another ParentDataWidget of the same type.\n'
332+
'Otherwise, separating aspects of ParentData to prevent conflicts can '
333+
'be done using mixins, mixing them all in on the full ParentData '
334+
'Object, such as KeepAlive does with KeepAliveParentDataMixin.\n'
335+
'The ownership chain for the RenderObject that received the parent data was:\n'
336+
' DecoratedBox ← Positioned ← SubclassPositioned ← Stack ← Directionality ← ', // End of chain omitted, not relevant for test.
337+
),
338+
);
339+
});
340+
253341
testWidgetsWithLeakTracking('ParentDataWidget conflicting data', (WidgetTester tester) async {
254342
await tester.pumpWidget(
255343
const Directionality(
256344
textDirection: TextDirection.ltr,
257345
child: Stack(
258-
textDirection: TextDirection.ltr,
259346
children: <Widget>[
260347
Positioned(
261348
top: 5.0,
@@ -277,19 +364,26 @@ void main() {
277364
exception.toString(),
278365
startsWith(
279366
'Incorrect use of ParentDataWidget.\n'
280-
'The following ParentDataWidgets are providing parent data to the same RenderObject:\n'
281-
'- Positioned(left: 7.0, top: 6.0) (typically placed directly inside a Stack widget)\n'
282-
'- Positioned(top: 5.0, bottom: 8.0) (typically placed directly inside a Stack widget)\n'
283-
'However, a RenderObject can only receive parent data from at most one ParentDataWidget.\n'
284-
'Usually, this indicates that at least one of the offending ParentDataWidgets listed '
285-
'above is not placed directly inside a compatible ancestor widget.\n'
367+
'Competing ParentDataWidgets are providing parent data to the same RenderObject:\n'
368+
'- Positioned(left: 7.0, top: 6.0), which writes ParentData of type '
369+
'StackParentData, (typically placed directly inside a Stack widget)\n'
370+
'- Positioned(top: 5.0, bottom: 8.0), which writes ParentData of type '
371+
'StackParentData, (typically placed directly inside a Stack widget)\n'
372+
'A RenderObject can receive parent data from multiple '
373+
'ParentDataWidgets, but the Type of ParentData must be unique to '
374+
'prevent one overwriting another.\n'
375+
'Usually, this indicates that one or more of the offending ParentDataWidgets listed '
376+
"above isn't placed inside a dedicated compatible ancestor widget that it isn't "
377+
'sharing with another ParentDataWidget of the same type.\n'
378+
'Otherwise, separating aspects of ParentData to prevent conflicts can '
379+
'be done using mixins, mixing them all in on the full ParentData '
380+
'Object, such as KeepAlive does with KeepAliveParentDataMixin.\n'
286381
'The ownership chain for the RenderObject that received the parent data was:\n'
287382
' DecoratedBox ← Positioned ← Positioned ← Stack ← Directionality ← ', // End of chain omitted, not relevant for test.
288383
),
289384
);
290385

291386
await tester.pumpWidget(const Stack(textDirection: TextDirection.ltr));
292-
293387
checkTree(tester, <TestParentData>[]);
294388

295389
await tester.pumpWidget(
@@ -308,6 +402,7 @@ void main() {
308402
),
309403
),
310404
);
405+
311406
exception = tester.takeException();
312407
expect(exception, isFlutterError);
313408
expect(
@@ -328,7 +423,6 @@ void main() {
328423
await tester.pumpWidget(
329424
const Stack(textDirection: TextDirection.ltr),
330425
);
331-
332426
checkTree(tester, <TestParentData>[]);
333427
});
334428

@@ -458,6 +552,46 @@ void main() {
458552
});
459553
}
460554

555+
class SubclassPositioned extends Positioned {
556+
const SubclassPositioned({
557+
super.key,
558+
super.left,
559+
super.top,
560+
super.right,
561+
super.bottom,
562+
super.width,
563+
super.height,
564+
required super.child,
565+
});
566+
567+
@override
568+
void applyParentData(RenderObject renderObject) {
569+
assert(renderObject.parentData is StackParentData);
570+
final StackParentData parentData = renderObject.parentData! as StackParentData;
571+
parentData.bottom = bottom;
572+
}
573+
}
574+
575+
class CustomPositionedWidget extends ParentDataWidget<StackParentData> {
576+
const CustomPositionedWidget({
577+
super.key,
578+
required this.bottom,
579+
required super.child,
580+
});
581+
582+
final double bottom;
583+
584+
@override
585+
void applyParentData(RenderObject renderObject) {
586+
assert(renderObject.parentData is StackParentData);
587+
final StackParentData parentData = renderObject.parentData! as StackParentData;
588+
parentData.bottom = bottom;
589+
}
590+
591+
@override
592+
Type get debugTypicalAncestorWidgetClass => Stack;
593+
}
594+
461595
class TestParentDataWidget extends ParentDataWidget<DummyParentData> {
462596
const TestParentDataWidget({
463597
super.key,

0 commit comments

Comments
 (0)