Thanks to visit codestin.com
Credit goes to github.com

Skip to content

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

Merged
merged 21 commits into from
Dec 3, 2021

Conversation

goderbauer
Copy link
Member

@goderbauer goderbauer commented Nov 23, 2021

Fixes #94075.

The newly provided SlottedMultiChildRenderObjectWidgetMixin and SlottedContainerRenderObjectMixin 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.

@flutter-dashboard flutter-dashboard bot added f: material design flutter/packages/flutter/material repository. framework flutter/packages/flutter repository. See also f: labels. labels Nov 23, 2021
@google-cla google-cla bot added the cla: yes label Nov 23, 2021
@goderbauer
Copy link
Member Author

/cc @HansMuller @justinmc @gspencergoog @Hixie I'm interested in what you think about this.

Copy link
Contributor

@HansMuller HansMuller left a 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!

@HansMuller
Copy link
Contributor

HansMuller commented Nov 23, 2021

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;
Copy link
Contributor

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?

Copy link
Member Author

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.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds good

@goderbauer
Copy link
Member Author

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.

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.).

@goderbauer goderbauer marked this pull request as ready for review November 24, 2021 00:02
@flutter-dashboard flutter-dashboard bot added d: api docs Issues with https://api.flutter.dev/ d: examples Sample code and demos documentation c: contributor-productivity Team-specific productivity, code health, technical debt. labels Nov 24, 2021
@gspencergoog
Copy link
Contributor

This looks really cool, something we've needed for a long time.

Copy link
Contributor

@justinmc justinmc left a 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);
Copy link
Contributor

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 :)

Copy link
Member Author

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.
Copy link
Contributor

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');
}
}

Copy link
Contributor

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
Copy link
Contributor

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 {
Copy link
Contributor

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?

Copy link
Member Author

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
Copy link
Contributor

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);
}

Copy link
Contributor

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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

values

Copy link
Contributor

@HansMuller HansMuller left a 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(
Copy link
Contributor

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.

Copy link
Member Author

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?

Copy link
Contributor

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 {
Copy link
Contributor

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;
Copy link
Contributor

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.

Copy link
Member Author

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);
Copy link
Contributor

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.

Copy link
Member Author

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?

Copy link
Contributor

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.
Copy link
Contributor

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;
Copy link
Contributor

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?

Copy link
Member Author

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(
Copy link
Contributor

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.
Copy link
Contributor

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(
Copy link
Contributor

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?

Copy link
Member Author

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 {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NICE

@goderbauer goderbauer force-pushed the SlottedRenderObjectWidget branch from fdf1afe to a7104e5 Compare December 1, 2021 18:13
Copy link
Contributor

@HansMuller HansMuller left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
c: contributor-productivity Team-specific productivity, code health, technical debt. d: api docs Issues with https://api.flutter.dev/ d: examples Sample code and demos f: material design flutter/packages/flutter/material repository. framework flutter/packages/flutter repository. See also f: labels.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Writing a RenderObjectWidget/RenderBox that organizes children in slots requires a lot of boilerplate
7 participants