Commit 6a5964d8 authored by Michael Goderbauer's avatar Michael Goderbauer Committed by Flutter GitHub Bot

Make ParentDataWidget usable with different ancestor RenderObjectWidget types (#48541)

parent 8d438b03
......@@ -955,7 +955,7 @@ class _PressableActionButtonState extends State<_PressableActionButton> {
// _ActionButtonParentData. _ActionButtonParentDataWidget is responsible for
// updating the pressed state of an _ActionButtonParentData based on the
// incoming isPressed property.
class _ActionButtonParentDataWidget extends ParentDataWidget<_CupertinoAlertActionsRenderWidget> {
class _ActionButtonParentDataWidget extends ParentDataWidget<_ActionButtonParentData> {
const _ActionButtonParentDataWidget({
Key key,
this.isPressed,
......@@ -977,6 +977,9 @@ class _ActionButtonParentDataWidget extends ParentDataWidget<_CupertinoAlertActi
targetParent.markNeedsPaint();
}
}
@override
Type get debugTypicalAncestorWidgetClass => _CupertinoAlertActionsRenderWidget;
}
// ParentData applied to individual action buttons that report whether or not
......
......@@ -1020,7 +1020,7 @@ class _PressableActionButtonState extends State<_PressableActionButton> {
// _ActionButtonParentData. _ActionButtonParentDataWidget is responsible for
// updating the pressed state of an _ActionButtonParentData based on the
// incoming [isPressed] property.
class _ActionButtonParentDataWidget extends ParentDataWidget<_CupertinoDialogActionsRenderWidget> {
class _ActionButtonParentDataWidget extends ParentDataWidget<_ActionButtonParentData> {
const _ActionButtonParentDataWidget({
Key key,
this.isPressed,
......@@ -1042,6 +1042,9 @@ class _ActionButtonParentDataWidget extends ParentDataWidget<_CupertinoDialogAct
targetParent.markNeedsPaint();
}
}
@override
Type get debugTypicalAncestorWidgetClass => _CupertinoDialogActionsRenderWidget;
}
// ParentData applied to individual action buttons that report whether or not
......
......@@ -5,6 +5,7 @@
import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/scheduler.dart';
import 'framework.dart';
......@@ -80,7 +81,7 @@ class _AutomaticKeepAliveState extends State<AutomaticKeepAlive> {
handle.addListener(_handles[handle]);
if (!_keepingAlive) {
_keepingAlive = true;
final ParentDataElement<SliverWithKeepAliveWidget> childElement = _getChildElement();
final ParentDataElement<KeepAliveParentDataMixin> childElement = _getChildElement();
if (childElement != null) {
// If the child already exists, update it synchronously.
_updateParentDataOfChild(childElement);
......@@ -92,7 +93,7 @@ class _AutomaticKeepAliveState extends State<AutomaticKeepAlive> {
if (!mounted) {
return;
}
final ParentDataElement<SliverWithKeepAliveWidget> childElement = _getChildElement();
final ParentDataElement<KeepAliveParentDataMixin> childElement = _getChildElement();
assert(childElement != null);
_updateParentDataOfChild(childElement);
});
......@@ -105,7 +106,7 @@ class _AutomaticKeepAliveState extends State<AutomaticKeepAlive> {
///
/// While this widget is guaranteed to have a child, this may return null if
/// the first build of that child has not completed yet.
ParentDataElement<SliverWithKeepAliveWidget> _getChildElement() {
ParentDataElement<KeepAliveParentDataMixin> _getChildElement() {
assert(mounted);
final Element element = context as Element;
Element childElement;
......@@ -131,12 +132,12 @@ class _AutomaticKeepAliveState extends State<AutomaticKeepAlive> {
element.visitChildren((Element child) {
childElement = child;
});
assert(childElement == null || childElement is ParentDataElement<SliverWithKeepAliveWidget>);
return childElement as ParentDataElement<SliverWithKeepAliveWidget>;
assert(childElement == null || childElement is ParentDataElement<KeepAliveParentDataMixin>);
return childElement as ParentDataElement<KeepAliveParentDataMixin>;
}
void _updateParentDataOfChild(ParentDataElement<SliverWithKeepAliveWidget> childElement) {
childElement.applyWidgetOutOfTurn(build(context) as ParentDataWidget<SliverWithKeepAliveWidget>);
void _updateParentDataOfChild(ParentDataElement<KeepAliveParentDataMixin> childElement) {
childElement.applyWidgetOutOfTurn(build(context) as ParentDataWidget<KeepAliveParentDataMixin>);
}
VoidCallback _createCallback(Listenable handle) {
......
......@@ -1910,7 +1910,7 @@ class CustomSingleChildLayout extends SingleChildRenderObjectWidget {
/// The [MultiChildLayoutDelegate.hasChild],
/// [MultiChildLayoutDelegate.layoutChild], and
/// [MultiChildLayoutDelegate.positionChild] methods use these identifiers.
class LayoutId extends ParentDataWidget<CustomMultiChildLayout> {
class LayoutId extends ParentDataWidget<MultiChildLayoutParentData> {
/// Marks a child with a layout identifier.
///
/// Both the child and the id arguments must not be null.
......@@ -1940,6 +1940,9 @@ class LayoutId extends ParentDataWidget<CustomMultiChildLayout> {
}
}
@override
Type get debugTypicalAncestorWidgetClass => CustomMultiChildLayout;
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
......@@ -3353,7 +3356,7 @@ class IndexedStack extends Stack {
/// * [PositionedTransition], which takes a provided [Animation] to transition
/// changes in the child's position over a given duration.
/// * [PositionedDirectional], which adapts to the ambient [Directionality].
class Positioned extends ParentDataWidget<Stack> {
class Positioned extends ParentDataWidget<StackParentData> {
/// Creates a widget that controls where a child of a [Stack] is positioned.
///
/// Only two out of the three horizontal values ([left], [right],
......@@ -3577,6 +3580,9 @@ class Positioned extends ParentDataWidget<Stack> {
}
}
@override
Type get debugTypicalAncestorWidgetClass => Stack;
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
......@@ -4363,7 +4369,7 @@ class Column extends Flex {
/// * [Expanded], which forces the child to expand to fill the available space.
/// * [Spacer], a widget that takes up space proportional to it's flex value.
/// * The [catalog of layout widgets](https://flutter.dev/widgets/layout/).
class Flexible extends ParentDataWidget<Flex> {
class Flexible extends ParentDataWidget<FlexParentData> {
/// Creates a widget that controls how a child of a [Row], [Column], or [Flex]
/// flexes.
const Flexible({
......@@ -4413,6 +4419,9 @@ class Flexible extends ParentDataWidget<Flex> {
}
}
@override
Type get debugTypicalAncestorWidgetClass => Flex;
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
......
......@@ -1419,9 +1419,8 @@ abstract class ProxyWidget extends Widget {
/// [RenderObjectWidget]s with more than one child. For example, [Stack] uses
/// the [Positioned] parent data widget to position each child.
///
/// A [ParentDataWidget] is specific to a particular kind of [RenderObject], and
/// thus also to a particular [RenderObjectWidget] class. That class is `T`, the
/// [ParentDataWidget] type argument.
/// A [ParentDataWidget] is specific to a particular kind of [ParentData]. That
/// class is `T`, the [ParentData] type argument.
///
/// {@tool snippet}
///
......@@ -1429,7 +1428,7 @@ abstract class ProxyWidget extends Widget {
/// `FrogJar` widget's children by specifying a [Size] for each one.
///
/// ```dart
/// class FrogSize extends ParentDataWidget<FrogJar> {
/// class FrogSize extends ParentDataWidget<FrogJarParentData> {
/// FrogSize({
/// Key key,
/// @required this.size,
......@@ -1449,6 +1448,9 @@ abstract class ProxyWidget extends Widget {
/// targetParent.markNeedsLayout();
/// }
/// }
///
/// @override
/// Type get debugTypicalAncestorWidgetClass => FrogJar;
/// }
/// ```
/// {@end-tool}
......@@ -1458,12 +1460,12 @@ abstract class ProxyWidget extends Widget {
/// * [RenderObject], the superclass for layout algorithms.
/// * [RenderObject.parentData], the slot that this class configures.
/// * [ParentData], the superclass of the data that will be placed in
/// [RenderObject.parentData] slots.
/// [RenderObject.parentData] slots. The `T` type parameter for
/// [ParentDataWidget] is a [ParentData].
/// * [RenderObjectWidget], the class for widgets that wrap [RenderObject]s.
/// The `T` type parameter for [ParentDataWidget] is a [RenderObjectWidget].
/// * [StatefulWidget] and [State], for widgets that can build differently
/// several times over their lifetime.
abstract class ParentDataWidget<T extends RenderObjectWidget> extends ProxyWidget {
abstract class ParentDataWidget<T extends ParentData> extends ProxyWidget {
/// Abstract const constructor. This constructor enables subclasses to provide
/// const constructors so that they can be used in const expressions.
const ParentDataWidget({ Key key, Widget child })
......@@ -1472,45 +1474,61 @@ abstract class ParentDataWidget<T extends RenderObjectWidget> extends ProxyWidge
@override
ParentDataElement<T> createElement() => ParentDataElement<T>(this);
/// Subclasses should override this method to return true if the given
/// ancestor is a RenderObjectWidget that wraps a RenderObject that can handle
/// the kind of ParentData widget that the ParentDataWidget subclass handles.
/// Checks if this widget can apply its parent data to the provided
/// `renderObject`.
///
/// The default implementation uses the type argument.
bool debugIsValidAncestor(RenderObjectWidget ancestor) {
/// The [RenderObject.parentData] of the provided `renderObject` is
/// typically set up by an ancestor [RenderObjectWidget] of the type returned
/// by [debugTypicalAncestorWidgetClass].
///
/// This is called just before [applyParentData] is invoked with the same
/// [RenderObject] provided to that method.
bool debugIsValidRenderObject(RenderObject renderObject) {
assert(T != dynamic);
assert(T != RenderObjectWidget);
return ancestor is T;
assert(T != ParentData);
return renderObject.parentData is T;
}
/// Subclasses should override this to describe the requirements for using the
/// ParentDataWidget subclass. It is called when debugIsValidAncestor()
/// returned false for an ancestor, or when there are extraneous
/// [ParentDataWidget]s in the ancestor chain.
Iterable<DiagnosticsNode> debugDescribeInvalidAncestorChain({ String description, DiagnosticsNode ownershipChain, bool foundValidAncestor, Iterable<Widget> badAncestors }) sync* {
/// The [RenderObjectWidget] that is typically used to set up the [ParentData]
/// that [applyParentData] will write to.
///
/// This is only used in error messages to tell users what widget typically
/// wraps this ParentDataWidget.
Type get debugTypicalAncestorWidgetClass;
Iterable<DiagnosticsNode> _debugDescribeIncorrectParentDataType({
@required ParentData parentData,
RenderObjectWidget parentDataCreator,
DiagnosticsNode ownershipChain,
}) sync* {
assert(T != dynamic);
assert(T != RenderObjectWidget);
if (!foundValidAncestor) {
assert(T != ParentData);
assert(debugTypicalAncestorWidgetClass != null);
final String description = 'The ParentDataWidget $this wants to apply ParentData of type $T to a RenderObject';
if (parentData == null) {
yield ErrorDescription(
'$runtimeType widgets must be placed inside $T widgets.\n'
'$description has no $T ancestor at all.'
'$description, which has not been set up to receive any ParentData.'
);
} else {
assert(badAncestors.isNotEmpty);
yield ErrorDescription(
'$runtimeType widgets must be placed directly inside $T widgets.\n'
'$description has a $T ancestor, but there are other widgets between them:'
'$description, which has been set up to accept ParentData of incompatible type ${parentData.runtimeType}.'
);
}
yield ErrorHint(
'Usually, this means that the $runtimeType widget has the wrong ancestor RenderObjectWidget. '
'Typically, $runtimeType widgets are placed directly inside $debugTypicalAncestorWidgetClass widgets.'
);
if (parentDataCreator != null) {
yield ErrorHint(
'The offending $runtimeType is currently placed inside a ${parentDataCreator.runtimeType} widget.'
);
}
if (ownershipChain != null) {
yield ErrorDescription(
'The ownership chain for the RenderObject that received the incompatible parent data was:\n $ownershipChain'
);
for (final Widget ancestor in badAncestors) {
if (ancestor.runtimeType == runtimeType) {
yield ErrorDescription('- $ancestor (this is a different $runtimeType than the one with the problem)');
} else {
yield ErrorDescription('- $ancestor');
}
}
yield ErrorDescription('These widgets cannot come between a $runtimeType and its $T.');
}
yield ErrorDescription('The ownership chain for the parent of the offending $runtimeType was:\n $ownershipChain');
}
/// Write the data from this widget into the given render object's parent data.
......@@ -2684,6 +2702,9 @@ class BuildOwner {
return true;
}());
} catch (e, stack) {
// Catching the exception directly to avoid activating the ErrorWidget.
// Since the tree is in a broken state, adding the ErrorWidget would
// cause more exceptions.
_debugReportException(ErrorSummary('while finalizing the widget tree'), e, stack);
} finally {
Timeline.finishSync();
......@@ -4657,52 +4678,19 @@ abstract class ProxyElement extends ComponentElement {
}
/// An [Element] that uses a [ParentDataWidget] as its configuration.
class ParentDataElement<T extends RenderObjectWidget> extends ProxyElement {
class ParentDataElement<T extends ParentData> extends ProxyElement {
/// Creates an element that uses the given widget as its configuration.
ParentDataElement(ParentDataWidget<T> widget) : super(widget);
@override
ParentDataWidget<T> get widget => super.widget as ParentDataWidget<T>;
@override
void mount(Element parent, dynamic newSlot) {
assert(() {
final List<Widget> badAncestors = <Widget>[];
Element ancestor = parent;
while (ancestor != null) {
if (ancestor is ParentDataElement<RenderObjectWidget>) {
badAncestors.add(ancestor.widget);
} else if (ancestor is RenderObjectElement) {
if (widget.debugIsValidAncestor(ancestor.widget))
break;
badAncestors.add(ancestor.widget);
}
ancestor = ancestor._parent;
}
if (ancestor != null && badAncestors.isEmpty)
return true;
// TODO(jacobr): switch to describing the invalid parent chain in terms
// of DiagnosticsNode objects when possible.
throw FlutterError.fromParts(<DiagnosticsNode>[
ErrorSummary('Incorrect use of ParentDataWidget.'),
// TODO(jacobr): fix this constructor call to use FlutterErrorBuilder.
...widget.debugDescribeInvalidAncestorChain(
description: '$this',
ownershipChain: ErrorDescription(parent.debugGetCreatorChain(10)),
foundValidAncestor: ancestor != null,
badAncestors: badAncestors,
),
]);
}());
super.mount(parent, newSlot);
}
void _applyParentData(ParentDataWidget<T> widget) {
void applyParentDataToChild(Element child) {
if (child is RenderObjectElement) {
child._updateParentData(widget);
} else {
assert(child is! ParentDataElement<RenderObjectWidget>);
assert(child is! ParentDataElement<ParentData>);
child.visitChildren(applyParentDataToChild);
}
}
......@@ -5116,14 +5104,48 @@ abstract class RenderObjectElement extends Element {
return ancestor as RenderObjectElement;
}
ParentDataElement<RenderObjectWidget> _findAncestorParentDataElement() {
ParentDataElement<ParentData> _findAncestorParentDataElement() {
Element ancestor = _parent;
ParentDataElement<ParentData> result;
while (ancestor != null && ancestor is! RenderObjectElement) {
if (ancestor is ParentDataElement<RenderObjectWidget>)
return ancestor;
if (ancestor is ParentDataElement<ParentData>) {
result = ancestor as ParentDataElement<ParentData>;
break;
}
ancestor = ancestor._parent;
}
return null;
assert(() {
if (result == null || ancestor == null) {
return true;
}
// Check that no other ParentDataWidgets want to provide parent data.
final List<ParentDataElement<ParentData>> badAncestors = <ParentDataElement<ParentData>>[];
ancestor = ancestor._parent;
while (ancestor != null && ancestor is! RenderObjectElement) {
if (ancestor is ParentDataElement<ParentData>) {
badAncestors.add(ancestor as ParentDataElement<ParentData>);
}
ancestor = ancestor._parent;
}
if (badAncestors.isNotEmpty) {
badAncestors.insert(0, result);
try {
throw FlutterError.fromParts(<DiagnosticsNode>[
ErrorSummary('Incorrect use of ParentDataWidget.'),
ErrorDescription('The following ParentDataWidgets are providing parent data to the same RenderObject:'),
for (final ParentDataElement<ParentData> ancestor in badAncestors)
ErrorDescription('- ${ancestor.widget} (typically placed directly inside a ${ancestor.widget.debugTypicalAncestorWidgetClass} widget)'),
ErrorDescription('However, a RenderObject can only receive parent data from at most one ParentDataWidget.'),
ErrorHint('Usually, this indicates that at least one of the offending ParentDataWidgets listed above is not placed directly inside a compatible ancestor widget.'),
ErrorDescription('The ownership chain for the RenderObject that received the parent data was:\n ${debugGetCreatorChain(10)}'),
]);
} on FlutterError catch (e) {
_debugReportException(ErrorSummary('while looking for parent data.'), e, e.stackTrace);
}
}
return true;
}());
return result;
}
@override
......@@ -5355,8 +5377,31 @@ abstract class RenderObjectElement extends Element {
widget.didUnmountRenderObject(renderObject);
}
void _updateParentData(ParentDataWidget<RenderObjectWidget> parentData) {
parentData.applyParentData(renderObject);
void _updateParentData(ParentDataWidget<ParentData> parentDataWidget) {
bool applyParentData = true;
assert(() {
try {
if (!parentDataWidget.debugIsValidRenderObject(renderObject)) {
applyParentData = false;
throw FlutterError.fromParts(<DiagnosticsNode>[
ErrorSummary('Incorrect use of ParentDataWidget.'),
...parentDataWidget._debugDescribeIncorrectParentDataType(
parentData: renderObject.parentData,
parentDataCreator: _ancestorRenderObjectElement.widget,
ownershipChain: ErrorDescription(debugGetCreatorChain(10)),
),
]);
}
} on FlutterError catch (e) {
// Catching the exception directly to avoid activating the ErrorWidget.
// Since the tree is in a broken state, adding the ErrorWidget would
// cause more exceptions.
_debugReportException(ErrorSummary('while applying parent data.'), e, e.stackTrace);
}
return true;
}());
if (applyParentData)
parentDataWidget.applyParentData(renderObject);
}
@override
......@@ -5373,7 +5418,7 @@ abstract class RenderObjectElement extends Element {
_slot = newSlot;
_ancestorRenderObjectElement = _findAncestorRenderObjectElement();
_ancestorRenderObjectElement?.insertChildRenderObject(renderObject, newSlot);
final ParentDataElement<RenderObjectWidget> parentDataElement = _findAncestorParentDataElement();
final ParentDataElement<ParentData> parentDataElement = _findAncestorParentDataElement();
if (parentDataElement != null)
_updateParentData(parentDataElement.widget);
}
......
......@@ -1554,7 +1554,7 @@ class _SliverOffstageElement extends SingleChildRenderObjectElement {
/// In practice, the simplest way to deal with these notifications is to mix
/// [AutomaticKeepAliveClientMixin] into one's [State]. See the documentation
/// for that mixin class for details.
class KeepAlive extends ParentDataWidget<SliverWithKeepAliveWidget> {
class KeepAlive extends ParentDataWidget<KeepAliveParentDataMixin> {
/// Marks a child as needing to remain alive.
///
/// The [child] and [keepAlive] arguments must not be null.
......@@ -1590,6 +1590,9 @@ class KeepAlive extends ParentDataWidget<SliverWithKeepAliveWidget> {
@override
bool debugCanApplyOutOfTurn() => keepAlive;
@override
Type get debugTypicalAncestorWidgetClass => SliverWithKeepAliveWidget;
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
......
......@@ -354,7 +354,7 @@ class _TableElement extends RenderObjectElement {
/// the [TableCell] widget to its enclosing [Table] must contain only
/// [TableRow]s, [StatelessWidget]s, or [StatefulWidget]s (not
/// other kinds of widgets, like [RenderObjectWidget]s).
class TableCell extends ParentDataWidget<Table> {
class TableCell extends ParentDataWidget<TableCellParentData> {
/// Creates a widget that controls how a child of a [Table] is aligned.
const TableCell({
Key key,
......@@ -376,6 +376,9 @@ class TableCell extends ParentDataWidget<Table> {
}
}
@override
Type get debugTypicalAncestorWidgetClass => Table;
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
......
......@@ -160,7 +160,7 @@ class InvalidConstraintsChildLayoutDelegate extends MultiChildLayoutDelegate {
bool shouldRelayout(MultiChildLayoutDelegate oldDelegate) => true;
}
class LayoutWithMissingId extends ParentDataWidget<CustomMultiChildLayout> {
class LayoutWithMissingId extends ParentDataWidget<MultiChildLayoutParentData> {
const LayoutWithMissingId({
Key key,
@required Widget child,
......@@ -169,6 +169,9 @@ class LayoutWithMissingId extends ParentDataWidget<CustomMultiChildLayout> {
@override
void applyParentData(RenderObject renderObject) {}
@override
Type get debugTypicalAncestorWidgetClass => CustomMultiChildLayout;
}
void main() {
......
......@@ -49,7 +49,6 @@ final TestParentData kNonPositioned = TestParentData();
void main() {
testWidgets('ParentDataWidget control test', (WidgetTester tester) async {
await tester.pumpWidget(
Stack(
textDirection: TextDirection.ltr,
......@@ -251,33 +250,39 @@ void main() {
testWidgets('ParentDataWidget conflicting data', (WidgetTester tester) async {
await tester.pumpWidget(
Stack(
Directionality(
textDirection: TextDirection.ltr,
children: const <Widget>[
Positioned(
top: 5.0,
bottom: 8.0,
child: Positioned(
top: 6.0,
left: 7.0,
child: DecoratedBox(decoration: kBoxDecorationB),
child: Stack(
textDirection: TextDirection.ltr,
children: const <Widget>[
Positioned(
top: 5.0,
bottom: 8.0,
child: Positioned(
top: 6.0,
left: 7.0,
child: DecoratedBox(decoration: kBoxDecorationB),
),
),
),
],
],
),
),
);
dynamic exception = tester.takeException();
expect(exception, isFlutterError);
expect(
exception.toString(),
equalsIgnoringHashCodes(
'Incorrect use of ParentDataWidget.\n'
'Positioned widgets must be placed directly inside Stack widgets.\n'
'Positioned(no depth, left: 7.0, top: 6.0, dirty) has a Stack ancestor, but there are other widgets between them:\n'
'- Positioned(top: 5.0, bottom: 8.0) (this is a different Positioned than the one with the problem)\n'
'These widgets cannot come between a Positioned and its Stack.\n'
'The ownership chain for the parent of the offending Positioned was:\n'
' Positioned ← Stack ← [root]'
'The following ParentDataWidgets are providing parent data to the same RenderObject:\n'
'- Positioned(left: 7.0, top: 6.0) (typically placed directly inside a Stack widget)\n'
'- Positioned(top: 5.0, bottom: 8.0) (typically placed directly inside a Stack widget)\n'
'However, a RenderObject can only receive parent data from at most one ParentDataWidget.\n'
'Usually, this indicates that at least one of the offending ParentDataWidgets listed '
'above is not placed directly inside a compatible ancestor widget.\n'
'The ownership chain for the RenderObject that received the parent data was:\n'
' DecoratedBox ← Positioned ← Positioned ← Stack ← Directionality ← [root]'
),
);
......@@ -286,15 +291,18 @@ void main() {
checkTree(tester, <TestParentData>[]);
await tester.pumpWidget(
Container(
child: Row(
children: const <Widget>[
Positioned(
top: 6.0,
left: 7.0,
child: DecoratedBox(decoration: kBoxDecorationB),
),
],
Directionality(
textDirection: TextDirection.ltr,
child: Container(
child: Row(
children: const <Widget>[
Positioned(
top: 6.0,
left: 7.0,
child: DecoratedBox(decoration: kBoxDecorationB),
),
],
),
),
),
);
......@@ -304,10 +312,14 @@ void main() {
exception.toString(),
equalsIgnoringHashCodes(
'Incorrect use of ParentDataWidget.\n'
'Positioned widgets must be placed inside Stack widgets.\n'
'Positioned(no depth, left: 7.0, top: 6.0, dirty) has no Stack ancestor at all.\n'
'The ownership chain for the parent of the offending Positioned was:\n'
' Row ← Container ← [root]'
'The ParentDataWidget Positioned(left: 7.0, top: 6.0) wants to apply ParentData of type '
'StackParentData to a RenderObject, which has been set up to accept ParentData of '
'incompatible type FlexParentData.\n'
'Usually, this means that the Positioned widget has the wrong ancestor RenderObjectWidget. '
'Typically, Positioned widgets are placed directly inside Stack widgets.\n'
'The offending Positioned is currently placed inside a Row widget.\n'
'The ownership chain for the RenderObject that received the incompatible parent data was:\n'
' DecoratedBox ← Positioned ← Row ← Container ← Directionality ← [root]'
),
);
......@@ -377,17 +389,20 @@ void main() {
});
testWidgets('Parent data invalid ancestor', (WidgetTester tester) async {
await tester.pumpWidget(Row(
children: <Widget>[
Stack(
textDirection: TextDirection.ltr,
children: <Widget>[
Expanded(
child: Container(),
),
],
),
],
await tester.pumpWidget(Directionality(
textDirection: TextDirection.ltr,
child: Row(
children: <Widget>[
Stack(
textDirection: TextDirection.ltr,
children: <Widget>[
Expanded(
child: Container(),
),
],
),
],
),
));
final dynamic exception = tester.takeException();
......@@ -396,13 +411,107 @@ void main() {
exception.toString(),
equalsIgnoringHashCodes(
'Incorrect use of ParentDataWidget.\n'
'Expanded widgets must be placed directly inside Flex widgets.\n'
'Expanded(no depth, flex: 1, dirty) has a Flex ancestor, but there are other widgets between them:\n'
'- Stack(alignment: AlignmentDirectional.topStart, textDirection: ltr, fit: loose, overflow: clip)\n'
'These widgets cannot come between a Expanded and its Flex.\n'
'The ownership chain for the parent of the offending Expanded was:\n'
' Stack ← Row ← [root]'
'The ParentDataWidget Expanded(flex: 1) wants to apply ParentData of type '
'FlexParentData to a RenderObject, which has been set up to accept ParentData of '
'incompatible type StackParentData.\n'
'Usually, this means that the Expanded widget has the wrong ancestor RenderObjectWidget. '
'Typically, Expanded widgets are placed directly inside Flex widgets.\n'
'The offending Expanded is currently placed inside a Stack widget.\n'
'The ownership chain for the RenderObject that received the incompatible parent data was:\n'
' LimitedBox ← Container ← Expanded ← Stack ← Row ← Directionality ← [root]'
),
);
});
testWidgets('ParentDataWidget can be used with different ancestor RenderObjectWidgets', (WidgetTester tester) async {
await tester.pumpWidget(
OneAncestorWidget(
child: Container(),
),
);
DummyParentData parentData = tester.renderObject(find.byType(Container)).parentData as DummyParentData;
expect(parentData.string, isNull);
await tester.pumpWidget(
OneAncestorWidget(
child: TestParentDataWidget(
string: 'Foo',
child: Container(),
),
),
);
parentData = tester.renderObject(find.byType(Container)).parentData as DummyParentData;
expect(parentData.string, 'Foo');
await tester.pumpWidget(
AnotherAncestorWidget(
child: TestParentDataWidget(
string: 'Bar',
child: Container(),
),
),
);
parentData = tester.renderObject(find.byType(Container)).parentData as DummyParentData;
expect(parentData.string, 'Bar');
});
}
class TestParentDataWidget extends ParentDataWidget<DummyParentData> {
const TestParentDataWidget({
Key key,
this.string,
Widget child,
}) : super(key: key, child: child);
final String string;
@override
void applyParentData(RenderObject renderObject) {
assert(renderObject.parentData is DummyParentData);
final DummyParentData parentData = renderObject.parentData as DummyParentData;
parentData.string = string;
}
@override
Type get debugTypicalAncestorWidgetClass => OneAncestorWidget;
}
class DummyParentData extends ParentData {
String string;
}
class OneAncestorWidget extends SingleChildRenderObjectWidget {
const OneAncestorWidget({
Key key,
Widget child,
}) : super(key: key, child: child);
@override
RenderOne createRenderObject(BuildContext context) => RenderOne();
}
class AnotherAncestorWidget extends SingleChildRenderObjectWidget {
const AnotherAncestorWidget({
Key key,
Widget child,
}) : super(key: key, child: child);
@override
RenderAnother createRenderObject(BuildContext context) => RenderAnother();
}
class RenderOne extends RenderProxyBox {
@override
void setupParentData(RenderBox child) {
if (child.parentData is! DummyParentData)
child.parentData = DummyParentData();
}
}
class RenderAnother extends RenderProxyBox {
@override
void setupParentData(RenderBox child) {
if (child.parentData is! DummyParentData)
child.parentData = DummyParentData();
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment