Skip to content

Conversation

@LongCatIsLooong
Copy link
Contributor

@LongCatIsLooong LongCatIsLooong commented Jul 1, 2022

Extracted from #105430

If you had to change anything in the flutter/tests repo, include a link to the migration guide as per the breaking change policy.

Pre-launch Checklist

  • I read the Contributor Guide and followed the process outlined there for submitting PRs.
  • I read the Tree Hygiene wiki page, which explains my responsibilities.
  • I read and followed the Flutter Style Guide, including Features we expect every widget to implement.
  • I signed the CLA.
  • I listed at least one issue that this PR fixes in the description above.
  • I updated/added relevant documentation (doc comments with ///).
  • I added new tests to check the change I am making, or this PR is test-exempt.
  • All existing and new tests are passing.

If you need help, consider asking for advice on the #hackers-new channel on Discord.

@flutter-dashboard flutter-dashboard bot added the framework flutter/packages/flutter repository. See also f: labels. label Jul 1, 2022
_updateChild(slottedMultiChildRenderObjectWidgetMixin.childForSlot(slot), slot);
final Widget? widget = slottedMultiChildRenderObjectWidgetMixin.childForSlot(slot);
final Key? newWidgetKey = widget?.key;
if (widget != null && newWidgetKey is GlobalKey) {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Regarding comments from the original pull request:

This now has me wondering: Don't we need to do this for all keys, not just globalkeys? If a keyed widget changes slots, it should be matched with the element from the previous slot it was in?

The documentation seems to suggest that should be the case:

/// A key that is not a [GlobalKey].
///
/// Keys must be unique amongst the [Element]s with the same parent. By
/// contrast, [GlobalKey]s must be unique across the entire app.

But I feel it to an extent overlaps the idea of the children being "slotted"

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I was under the impression that to be able to use a local key the children have to be somewhat homogeneous, like in a list view or a column, but that doesn't seem to be true according to the documentation.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Replaced GlobalKey with Key.

@LongCatIsLooong LongCatIsLooong changed the title Allow global key reparenting between slots in SlottedMultiChildRenderObjectWidgetMixin Allow key reparenting between slots in SlottedMultiChildRenderObjectWidgetMixin Jul 1, 2022
}

void _moveChild(RenderBox child, S slot, S oldSlot) {
final RenderBox? oldChild = _slotToChild[oldSlot];
Copy link
Member

Choose a reason for hiding this comment

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

nit: throw in an assert that slot != oldSlot?

// reparenting first.
for (final S slot in slottedMultiChildRenderObjectWidgetMixin.slots) {
_updateChild(slottedMultiChildRenderObjectWidgetMixin.childForSlot(slot), slot);
final Widget? widget = slottedMultiChildRenderObjectWidgetMixin.childForSlot(slot);
Copy link
Member

Choose a reason for hiding this comment

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

I dislike that we call childForSlot twice for unkeyed widgets. While the API doesn't make any promises how often this gets called, it my be expensive to construct the widget for a slot, so we should avoid this duplication...

Copy link
Member

Choose a reason for hiding this comment

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

Caching would be one option. We may also be able to do this in one pass as long as we don't match existing keyed widget with new unkeyed ones in case the keyed ones move to a new slot further down the line...

Copy link
Contributor Author

Choose a reason for hiding this comment

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

as long as we don't match existing keyed widget with new unkeyed ones

There's nothing that prevents this I think?

Copy link
Contributor Author

@LongCatIsLooong LongCatIsLooong Jul 7, 2022

Choose a reason for hiding this comment

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

Also I think the type parameter S should extend Object?

Copy link
Member

Choose a reason for hiding this comment

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

Also I think the type parameter S should extend Object?

Why?

Copy link
Member

Choose a reason for hiding this comment

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

Regarding the one-pass, I was thinking we could do something like this (pseudo-code):

for (final S slot in slottedMultiChildRenderObjectWidgetMixin.slots) {
  final Widget? widget = slottedMultiChildRenderObjectWidgetMixin.childForSlot(slot);
  if (widget.key != null) {
    var oldChild = oldKeyToElement.remove(widget.key);
    if (oldChild == null) {
      oldChild = _slotToChild[slot].widget.key != null : _slotToChild[slot] : null;
    } else if (oldChild == _slotToChild[oldChild.slot]) {
       _slotToChild.remove(oldChild.slot);
    }
    var newChild = updateChild(oldChild, widget, slot);
    _slotToChild[slot] = newChild;
  } else {
   var oldChild = _slotToChild[slot].widget.key == null ? _slotToChild[slot] : null;
   var newChild = updateChild(oldChild, widget, slot);
   _slotToChild[slot] = newChild;
  }
}
// TODO: match the remaining children in oldKeyToElement to null with `updateChild`.

I am sure I messed up some details in that code, but something like that?

Copy link
Contributor Author

@LongCatIsLooong LongCatIsLooong Jul 15, 2022

Choose a reason for hiding this comment

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

Why?

With that Key to S map I'm making the assumption that S cannot be null (https://master-api.flutter.dev/flutter/dart-core/Map/operator_get.html) so if people use null as a slot it could mess up the key to slot lookup. I can use containsKey but it will be a bit hard to read.

one-pass

Ahh I see if oldWidget.key != null and newWidget.key == null Widget.canUpdate will return false and we'll inflate a new element anyways. But if that happens and the old keyed Elemenent doesn't end up getting updated by a new widget we'll have to call deactivate on it manually, so we kinda need a new collection to do the bookkeeping anyway?

Copy link
Member

Choose a reason for hiding this comment

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

so if people use null as a slot it could mess up the key to slot lookup.

Good point. In theory, null is a valid slot value, which I think the current implementation fully supports. However, if changing this isn't breaking anything (and it really simplifies the implementation a lot - which I am not fully convinced yet) I would be ok with changing this and documenting that limitation.

But if that happens and the old keyed Element doesn't end up getting updated by a new widget we'll have to call deactivate on it manually, so we kinda need a new collection to do the bookkeeping anyway?

Not sure I understand. If we pass an Element to updateChild it will either get re-used or properly deactivated.

Oh, but I think I missed a case in my code above (maybe that's the one you meant): When a keyed child moves into a slot previously occupied by an unkeyed child, we need to deactivate that child by calling updateChild(unkeyedChild, null, slot) for it before updating the slot of the keyed child. But I think we can just do that right before the first var newChild = updateChild(oldChild, widget, slot); occurence in the code above?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Updated. Also tried to avoid using S as map value type.

}

Element _updateSlotForKeyedChild(Widget widget, S oldSlot, S newSlot) {
final Element? oldChild = _slotToChild.remove(oldSlot);
Copy link
Member

Choose a reason for hiding this comment

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

nit: assert that widget has a key?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

inlined the method.

final Widget? widget = slottedMultiChildRenderObjectWidgetMixin.childForSlot(slot);
final Key? newWidgetKey = widget?.key;
if (newWidgetKey != null) {
assert(!_keyToSlot.containsKey(newWidgetKey));
Copy link
Member

Choose a reason for hiding this comment

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

This assert should probably get a better error message telling the user that duplicated keys are not allowed and giving them some information what the widgets with duplicated keys are. We have a duplicated key error elsewhere, that you could probably reuse here.

}
}

_slotToChild.addAll(slotToKeyedChild);
Copy link
Member

Choose a reason for hiding this comment

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

Why do we need the indirection via slotToKeyedChild? Could these just be added to _slotToChild?

// New slots for keyed children. It shouldn't be updated in place because
// two children could swap slots.
final Map<S, Element> slotToKeyedChild = <S, Element>{};
final List<MapEntry<S, Widget?>> unkeyedWidgets = <MapEntry<S, Widget?>>[];
Copy link
Member

Choose a reason for hiding this comment

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

Can this just be a map? Maybe a LinkedHashMap if insert order needs to be preserved.

Comment on lines 259 to 262
final Element newChild = updateChild(_slotToChild.remove(fromSlot), widget, slot)!;
assert(!_slotToChild.containsKey(fromSlot));
assert(!slotToKeyedChild.containsKey(slot));
slotToKeyedChild[slot] = newChild;
Copy link
Member

Choose a reason for hiding this comment

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

Couldn't this (combined with some of the other comments) just re-use the logic in _updateChild?

Copy link
Member

Choose a reason for hiding this comment

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

(We probably have to remove the assert in that method that wasn't valid anyways since you have to delete the justification comment there...)

// reparenting first.
for (final S slot in slottedMultiChildRenderObjectWidgetMixin.slots) {
_updateChild(slottedMultiChildRenderObjectWidgetMixin.childForSlot(slot), slot);
final Widget? widget = slottedMultiChildRenderObjectWidgetMixin.childForSlot(slot);
Copy link
Member

Choose a reason for hiding this comment

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

Also I think the type parameter S should extend Object?

Why?

if (newWidgetKey != null) {
assert(!_keyToSlot.containsKey(newWidgetKey));
_keyToSlot[newWidgetKey] = slot;
final S fromSlot = oldKeyedSlots[newWidgetKey] ?? slot;
Copy link
Member

Choose a reason for hiding this comment

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

Why can we just use slot as fromSlot? Isn't it possible that slot contains another keyed widget that might move to another slot behind slot? E.g. what if we have
slots A, B
keyed widget K1, K2
unkeyed widget W

We start out with the following slot occupancy:

A: K1
B: W

In the next frame we want to go to:

A: K2
B: K1

I believe the current code would match (via updateChild) K1 with K2, which means K2 is not actually moved to B. Instead, a new element is created for K1 in slot B.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah the updated test caught this, the elements were not reused.


// Swapping.
await tester.pumpWidget(buildWidget(topLeft: widget2, bottomRight: widget1));
expect(renderObject._topLeft!.size, const Size(100, 100));
Copy link
Member

Choose a reason for hiding this comment

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

These tests need to assert that the elements backing the widgets are still the same after the swap/move. That's the main indicator that something was actually moved and not just destroyed and recreated.

// reparenting first.
for (final S slot in slottedMultiChildRenderObjectWidgetMixin.slots) {
_updateChild(slottedMultiChildRenderObjectWidgetMixin.childForSlot(slot), slot);
final Widget? widget = slottedMultiChildRenderObjectWidgetMixin.childForSlot(slot);
Copy link
Member

Choose a reason for hiding this comment

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

Regarding the one-pass, I was thinking we could do something like this (pseudo-code):

for (final S slot in slottedMultiChildRenderObjectWidgetMixin.slots) {
  final Widget? widget = slottedMultiChildRenderObjectWidgetMixin.childForSlot(slot);
  if (widget.key != null) {
    var oldChild = oldKeyToElement.remove(widget.key);
    if (oldChild == null) {
      oldChild = _slotToChild[slot].widget.key != null : _slotToChild[slot] : null;
    } else if (oldChild == _slotToChild[oldChild.slot]) {
       _slotToChild.remove(oldChild.slot);
    }
    var newChild = updateChild(oldChild, widget, slot);
    _slotToChild[slot] = newChild;
  } else {
   var oldChild = _slotToChild[slot].widget.key == null ? _slotToChild[slot] : null;
   var newChild = updateChild(oldChild, widget, slot);
   _slotToChild[slot] = newChild;
  }
}
// TODO: match the remaining children in oldKeyToElement to null with `updateChild`.

I am sure I messed up some details in that code, but something like that?

assert(renderObject._slotToChild[slot] == child);
renderObject._setChild(null, slot);
assert(renderObject._slotToChild[slot] == null);
if (renderObject._slotToChild[slot] == child) {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

had to change this part as a result of the delayed deactivateChild call.

Copy link
Member

@goderbauer goderbauer left a comment

Choose a reason for hiding this comment

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

LGTM

Comment on lines 162 to 164
expect(widget1Element, tester.element(find.byWidget(widget1)));
expect(widget2Element, tester.element(find.byWidget(widget2)));
expect(nullWidgetElement, tester.element(find.byWidget(nullWidget)));
Copy link
Member

Choose a reason for hiding this comment

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

nit: for added clarity you could optionally use the same matcher here.

@LongCatIsLooong LongCatIsLooong added the autosubmit Merge PR when tree becomes green via auto submit App label Jul 15, 2022
@auto-submit auto-submit bot merged commit 0d32ca2 into flutter:master Jul 15, 2022
@LongCatIsLooong LongCatIsLooong deleted the SlottedMultiChildRenderObjectWidgetMixin-globalkey branch July 15, 2022 20:00
engine-flutter-autoroll added a commit to engine-flutter-autoroll/plugins that referenced this pull request Jul 16, 2022
engine-flutter-autoroll added a commit to engine-flutter-autoroll/packages that referenced this pull request Jul 16, 2022
engine-flutter-autoroll added a commit to engine-flutter-autoroll/packages that referenced this pull request Jul 19, 2022
engine-flutter-autoroll added a commit to engine-flutter-autoroll/packages that referenced this pull request Jul 19, 2022
engine-flutter-autoroll added a commit to engine-flutter-autoroll/packages that referenced this pull request Jul 19, 2022
engine-flutter-autoroll added a commit to engine-flutter-autoroll/packages that referenced this pull request Jul 19, 2022
engine-flutter-autoroll added a commit to engine-flutter-autoroll/packages that referenced this pull request Jul 21, 2022
engine-flutter-autoroll added a commit to engine-flutter-autoroll/packages that referenced this pull request Jul 22, 2022
engine-flutter-autoroll added a commit to engine-flutter-autoroll/packages that referenced this pull request Jul 22, 2022
engine-flutter-autoroll added a commit to engine-flutter-autoroll/packages that referenced this pull request Jul 22, 2022
engine-flutter-autoroll added a commit to engine-flutter-autoroll/packages that referenced this pull request Jul 22, 2022
engine-flutter-autoroll added a commit to engine-flutter-autoroll/packages that referenced this pull request Jul 22, 2022
engine-flutter-autoroll added a commit to engine-flutter-autoroll/packages that referenced this pull request Jul 23, 2022
engine-flutter-autoroll added a commit to engine-flutter-autoroll/packages that referenced this pull request Jul 23, 2022
engine-flutter-autoroll added a commit to engine-flutter-autoroll/packages that referenced this pull request Jul 24, 2022
engine-flutter-autoroll added a commit to engine-flutter-autoroll/packages that referenced this pull request Jul 24, 2022
engine-flutter-autoroll added a commit to engine-flutter-autoroll/packages that referenced this pull request Jul 25, 2022
engine-flutter-autoroll added a commit to engine-flutter-autoroll/packages that referenced this pull request Jul 25, 2022
engine-flutter-autoroll added a commit to engine-flutter-autoroll/packages that referenced this pull request Jul 25, 2022
engine-flutter-autoroll added a commit to engine-flutter-autoroll/packages that referenced this pull request Jul 25, 2022
engine-flutter-autoroll added a commit to engine-flutter-autoroll/packages that referenced this pull request Jul 25, 2022
engine-flutter-autoroll added a commit to engine-flutter-autoroll/packages that referenced this pull request Jul 25, 2022
engine-flutter-autoroll added a commit to engine-flutter-autoroll/packages that referenced this pull request Jul 25, 2022
engine-flutter-autoroll added a commit to engine-flutter-autoroll/packages that referenced this pull request Jul 25, 2022
engine-flutter-autoroll added a commit to engine-flutter-autoroll/packages that referenced this pull request Jul 25, 2022
engine-flutter-autoroll added a commit to engine-flutter-autoroll/packages that referenced this pull request Jul 25, 2022
engine-flutter-autoroll added a commit to engine-flutter-autoroll/packages that referenced this pull request Jul 25, 2022
engine-flutter-autoroll added a commit to engine-flutter-autoroll/packages that referenced this pull request Jul 25, 2022
engine-flutter-autoroll added a commit to engine-flutter-autoroll/packages that referenced this pull request Jul 26, 2022
engine-flutter-autoroll added a commit to engine-flutter-autoroll/packages that referenced this pull request Jul 26, 2022
engine-flutter-autoroll added a commit to engine-flutter-autoroll/packages that referenced this pull request Jul 26, 2022
engine-flutter-autoroll added a commit to engine-flutter-autoroll/packages that referenced this pull request Jul 26, 2022
engine-flutter-autoroll added a commit to engine-flutter-autoroll/packages that referenced this pull request Jul 26, 2022
engine-flutter-autoroll added a commit to engine-flutter-autoroll/packages that referenced this pull request Jul 26, 2022
camsim99 pushed a commit to camsim99/flutter that referenced this pull request Aug 10, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

autosubmit Merge PR when tree becomes green via auto submit App framework flutter/packages/flutter repository. See also f: labels.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants