-
Notifications
You must be signed in to change notification settings - Fork 28.7k
Mixin for slotted RenderObjectWidgets and RenderBox #94077
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
Mixin for slotted RenderObjectWidgets and RenderBox #94077
Conversation
/cc @HansMuller @justinmc @gspencergoog @Hixie I'm interested in what you think about this. |
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 great, it makes dealing with _ChipSlot a "chip shot" :-). Really: the updates to Chip et al. are very compelling; nicely done!
Even though our code base is now littered with examples of the new mixins in action, it would be helpful to include an - as simple as possible - example along with them. Developers are likely to want to start by copy and pasting something and then hacking it up. Best if they don't have prune InputDecorator (or whatever) for that. |
@@ -2025,7 +2025,19 @@ class _ChipRenderWidget extends RenderObjectWidget { | |||
final ShapeBorder? avatarBorder; | |||
|
|||
@override | |||
_RenderChipElement createElement() => _RenderChipElement(this); | |||
Iterable<_ChipSlot> get slots => _ChipSlot.values; |
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.
What is the benefit of using slot vs a list of child where the index is the slot? can it be a special version of multichildrenderobjectwidget where the element can be 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.
It's a different mental model. For the MultiChildRenderObjectWidget, the children are more or less uniform and have the same semantic meaning (e.g. entries in a row). Whereas in a slotted render object, the children in different slots can have different shapes and different semantic meaning (e.g. a list tile has a slot for a trailing/leading icon and a title, etc.). In the MultiChildRenderObjectWidget case, children are interchangeable (the first item in a row can easily switch with the second one and vice versa). For a slotted object that often doesn't make sense (the leading icon of a ListTile doesn't make sense to be the title).
Of course, the slotted model can be mapped to the indexed model, but now you have the additional overhead of keeping a map between a semantic slot and its index.
And then there's the fact that the MultiChildRenderObjectWidget is especially engineered to not allow null-children. In the slotted world null is totally acceptable for a given slot. Of course, you could work around that, but it would make the mapping between slots and indexes even more complex.
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.
Sounds good
Agreed. I am working on one. While doing so I realized that the SlottedContainerRenderObjectMixin can do even more lifting (e.g. attach/detach children, etc.). |
This looks really cool, something we've needed for a long time. |
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
Looks like this really helps simplify the input decoration stuff, I'm all on board with it 👍
// of the top left child. | ||
Size bottomRightSize = Size.zero; | ||
if (_bottomRight != null) { | ||
_bottomRight!.layout(childConstraints, parentUsesSize: true); |
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.
The bottomRight child is constrained to the size of the topLeft child? Just making sure that's what you intended. Above on the first line of this method you comment that they are both unconstrained.
I think I'm nitpicking unimportant things, I know this is just an example :)
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.
childConstraints
is initialized to BoxConstraints()
, which defaults to unconstraint constraints. So, bottomRight doesn't get to know anything about the size of topRight. It should therefore be unconstraint.
bottomRightSize = _bottomRight!.size; | ||
} | ||
|
||
// Calculate the overall size and constrains it to the given constraints. |
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.
"constrains it" => "constrain it"
assert(false, 'not reachable'); | ||
} | ||
} | ||
|
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.
Well it's immediately clear how much unnecessary code this is saving 👍
/// A mixin for a [RenderObjectWidget] that configures a [RenderObject] | ||
/// subclass, which organizes its children in different slots. | ||
/// | ||
/// Implementors of this mixin have to provide the list of available slots by |
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.
"Implementor" => "Implementer"
I thought there was something about this in the style guide but I couldn't find it. Anyway, grepping through the rest of the framework code I see that it's always spelled with "er".
/// with a single list of children. | ||
/// * [ListTile], which uses [SlottedMultiChildRenderObjectWidgetMixin] in its | ||
/// internal (private) implementation. | ||
mixin SlottedMultiChildRenderObjectWidgetMixin<S> on RenderObjectWidget { |
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 is the type parameter S
instead of T
?
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.
S
for Slots :)
/// The [RenderBox] child currently occupying a given slot can be obtained by | ||
/// calling [childForSlot]. | ||
/// | ||
/// Implementors may consider overriding [children] to return the children |
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.
Another "or" => "er".
@override | ||
String toString() => describeIdentity(this); | ||
} | ||
|
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.
Delete this empty line here.
/// implementing this mixin. | ||
/// | ||
/// Typically, an [Enum] is used to identify the different slots. In that case | ||
/// this getter can be implemented by returning what the `value` getter |
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.
values
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.
[sorry] I haven't read all of this yet
} | ||
|
||
@override | ||
SlottedContainerRenderObjectMixin<_DiagonalSlot> createRenderObject( |
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 to briefly explain why createRenderObject and updateRenderObject copy the widget's configuration to the render object, except for the widget's children.
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 took a stab at writing an explanation. Does this make it clear?
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, it's clear enough now. A developer with a burning desire to know more can follow the path from here pretty easily :-)
import 'package:flutter/rendering.dart'; | ||
|
||
/// Slots used for the children of [_Diagonal] and [_RenderDiagonal]. | ||
enum _DiagonalSlot { |
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 are the types in this example private? There's no harm in it, but the code might be a little easier on the eyes if everything was public.
class _RenderDiagonal extends RenderBox with SlottedContainerRenderObjectMixin<_DiagonalSlot>, DebugOverflowIndicatorMixin { | ||
_RenderDiagonal({Color? backgroundColor}) : _backgroundColor = backgroundColor; | ||
|
||
Color? get backgroundColor => _backgroundColor; |
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 might be worth noting that this property's implementation is boilerplate that matches the corresponding widget property. The only decision the developer actually has to make is choosing markNeedsLayout vs markNeedsPaint.
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.
Added.
// Lay out the top left child and position it at offset zero. | ||
Size topLeftSize = Size.zero; | ||
if (_topLeft != null) { | ||
_topLeft!.layout(childConstraints, parentUsesSize: true); |
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's odd that we have to use _topLeft!
instead of topLeft
.
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.
Assigned it to a local variable. Does that look cleaner?
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. I hadn't understood why _topLeft!
was needed. I guess the way the code was, the compiler couldn't guarantee that _topLeft wouldn't be reset to null as a result of some side effect within the body of if (_topLeft != null) { ... }
.
); | ||
} | ||
|
||
// Paint the children at the offset calculated during layout. |
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 a little bit confusing since there's an offset
parameter and you're referring to each child's (child.parentData! as BoxParentData).offset
. It's obvious if you look at the _paintChild method - maybe just make paintChild() a local function.
void paintChild(RenderBox? child) {
if (child != null) {
final BoxParentData childParentData = child.parentData! as BoxParentData;
context.paintChild(child, childParentData.offset + offset);
}
}
paintChild(_topLeft);
paintChild(_bottomRight);
// HIT TEST | ||
|
||
@override | ||
bool hitTestSelf(Offset position) => true; |
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 override needed for the example? If false is returned, hitTestChildren() will be checked?
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's not needed for this example and I removed it.
It controls how Diagonal behaves when wrapped in a GestureDetector. When tapping the bottomLeft or topRight corner (that's not covered by one of the children) should that trigger the GestureDetector or not? Without this, it will not. Depending on the use case that's a fine choice.
bool hitTestChildren(BoxHitTestResult result, { required Offset position }) { | ||
for (final RenderBox child in children) { | ||
final BoxParentData parentData = child.parentData! as BoxParentData; | ||
final bool isHit = result.addWithPaintOffset( |
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's a shame that "addWithPaintOffset()" effectively means hit test the child.
RenderBox? get _bottomRight => childForSlot(_DiagonalSlot.bottomRight); | ||
|
||
// The size this render object would have if the incoming constraints were | ||
// unconstrained; calculated during performLayout. |
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 mention that this field is just here to support an assertion that checks for overflow at paint time.
home: Scaffold( | ||
appBar: AppBar(title: const Text('Slotted RenderObject Example')), | ||
body: Center( | ||
child: _Diagonal( |
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 looks like more of this widget tree could be const?
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.
Unfortunately, Container
doesn't have a const constructor because some of its constructor asserts use non-const values. I could make this all const by replacing the Container
widget with a SizedBox nested with a ColoredBox, but I think that would make the example just slightly more complicated as more people will be familiar with Container
over those two widgets.
import 'package:flutter_test/flutter_test.dart'; | ||
|
||
void main() { | ||
testWidgets('shows two widgets arranged diagonally', (WidgetTester tester) async { |
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.
NICE
fdf1afe
to
a7104e5
Compare
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
This reverts commit 988959d.
…94077)" Test was fixed in flutter#94624.
Fixes #94075.
The newly provided
SlottedMultiChildRenderObjectWidgetMixin
andSlottedContainerRenderObjectMixin
make it easier to implement RenderObjectWidgets, who have multiple children that are organized in slots. In particular, it removes the need to implement a custom Element.To show how this can simplify these kind of render object widgets, the ListTile, Chip, and Decoration widget have been refactored to use the new concepts.