-
Notifications
You must be signed in to change notification settings - Fork 29.7k
Make ParentDataWidget usable with different ancestor RenderObjectWidget types #48541
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
| /// * [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 { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this not a breaking change?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes and No. :)
As far as I can tell this will not break any tests. However, I will be writing a migration guide for people that have custom ParentDataWidgets (see PR description :P )
| if (ancestor is ParentDataElement<RenderObjectWidget>) | ||
| return ancestor; | ||
| if (ancestor is ParentDataElement<ParentData>) { | ||
| result = ancestor as ParentDataElement<ParentData>; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I believe that either the as isn't necessary here anymore, or that if it is you should just be doing:
result = ancestor as ParentDataElement<ParentData>;
if (result != null) {
break;There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Without the "as" I am getting:
error: A value of type 'Element' can't be assigned to a variable of type 'ParentDataElement'. (invalid_assignment at [flutter] lib/src/widgets/framework.dart:5096)
And in your code, the "as" would throw if ancestor is not a ParentDataElement, which I don't want.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ahh I was thinking of C#. Nevermind
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
i'm surprised you need the as, i expected this case to be handled. cc @leafpetersen
| 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.debugTypicalAncestorWidget} 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); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why isn't this just
_debugReportException(ErrorSummary('while looking for parent data.'), FlutterError.fromParts(...), StackTrace.current);?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
see below.
| } | ||
| return null; | ||
| assert(() { | ||
| if (result == null || ancestor == null) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is it ever possible for the result to have been the root? Or is that condition acceptable?
e.g. if you somehow get to the top most ancestor, and it is the wrong type, we'd have result != null && ancestor == null here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't fully understand what you mean, if we never find the correct type, result would be null as well?
Also, it is fine to not find any ancestor ParentDataElement.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I mean if you find it and it's the root render object
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe just q comment then about why we are throwing and catching this way would help
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I mean if you find it and it's the root render object
You mean, the root of the element tree? That's fine for the purpose of this check. This check only wants to check how many ParentDataElements there are between this RenderObjectElement and the next RenderObjectElement (or the root).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ahh ok
| void _updateParentData(ParentDataWidget<ParentData> parentDataWidget) { | ||
| bool applyParentData = true; | ||
| assert(() { | ||
| try { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ditto here. We should avoid using exceptions for flow control - it causes debuggers to fire here when users are debugging their own code.
Or is that whta we want? If so, we should add a comment to that effect.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is not an exception for control flow. This throws when there's actually something wrong with your app, so if you are debugging your app and you have it set up to fire on exception, you want it to fire here.
I am only catching the exception here directly, because I don't want the exception to bubble up and cause the error widget to swoop in because the tree is in a state where the error widget would cause more exceptions...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe just q comment then about why we are throwing and catching this way would help
Will do.
| Iterable<DiagnosticsNode> debugDescribeInvalidAncestorChain({ String description, DiagnosticsNode ownershipChain, bool foundValidAncestor, Iterable<Widget> badAncestors }) sync* { | ||
| /// The [RenderObjectWidget] that is typically used to setup the [ParentData] | ||
| /// that [applyParentData] will write to. | ||
| Type get debugTypicalAncestorWidget; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It would be helpful if this docstring explained that this is only used for error message purposes - i.e. it is not actually used for comparison in asserts or anything.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done.
| } | ||
|
|
||
| @override | ||
| Type get debugTypicalAncestorWidget => _CupertinoAlertActionsRenderWidget; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Here and elsewhere - does it make any difference to wrap these in asserts so that they're null outside of debug mode?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since they are only called from on assert, these methods should already be tree-shaken out in release mode.
dnfield
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Lgtm
| bool debugIsValidAncestor(RenderObjectWidget ancestor) { | ||
| /// Checks if this widget can apply its parent data to the provided | ||
| /// `renderObject`. | ||
| bool debugIsValidRenderObject(RenderObject renderObject) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
docs should probably mention debugTypicalAncestorWidget and also mention when this is called in the widget lifecycle
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done.
| /// | ||
| /// This is only used in error messages to tell users what widget typically | ||
| /// wraps this ParentDataWidget. | ||
| Type get debugTypicalAncestorWidget; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
debugTypicalAncestorWidgetClass? or is that too verbose
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Changed.
| yield ErrorDescription( | ||
| '$runtimeType widgets must be placed inside $T widgets.\n' | ||
| '$description has no $T ancestor at all.' | ||
| '$description, which has not been setup to receive any ParentData.' |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
set up, two words
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done.
| 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 setup to only accept ParentData of incompatible type ${parentData.runtimeType}.' |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ditto
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
i'd probably remove "only" here
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done.
Includes fix for Flutter v1.14.0+. Breaking change described here: flutter/flutter#48541
Description
Prior to this change, a ParentDataWidget was bound to a particular ancestor RenderObjectWidget type, whose RenderObject was responsible for setting up the parent data that the ParentDataWidget would write to. A given ParentDataWidget could not be re-used with another RenderObjectWidget type as ancestor. For example, a new implementation of Stack (e.g.
SuperStackwidget) was not able to re-use the Positioned widget, because it was bound to the existing Stack widget.This change loosens the contract between ParentDataWidget and ancestor RenderObjectWidget types. Any RenderObjectWidget type can be used as an ancestor for a ParentDataWidget as long as the RenderObject of the RenderObjectWidget sets up the ParentData type expected by the ParentDataWidget.
Related Issues
Required for fixing #45797.
Tests
I added the following tests:
Checklist
Before you create this PR confirm that it meets all requirements listed below by checking the relevant checkboxes (
[x]). This will ensure a smooth and quick review process.///).flutter analyze --flutter-repo) does not report any problems on my PR.Breaking Change
Did any tests fail when you ran them? Please read Handling breaking changes.
AFIAK, this is not a breaking change as defined in the breaking change policy, but since this may effect some users negatively, I am providing a migration guide: flutter/website#3525.