Skip to content

Conversation

@Piinks
Copy link
Contributor

@Piinks Piinks commented Aug 1, 2023

Fixes #126297

This adds support for keep alive to the 2D scrolling foundation. The TwoDimensionalChildBuilderDelegate and TwoDimensionalChildListDelegate will both add automatic keep alives to their children, matching the convention from SliverChildDelegates. The TwoDimensionalViewportParentData now incorporates keep alive and which is managed by the RenderTwoDimensionalViewport.

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.

@Piinks Piinks added c: new feature Nothing broken; request for a new capability framework flutter/packages/flutter repository. See also f: labels. f: scrolling Viewports, list views, slivers, etc. labels Aug 1, 2023
@Piinks Piinks requested a review from chunhtai August 1, 2023 00:05
Copy link
Contributor

@chunhtai chunhtai left a comment

Choose a reason for hiding this comment

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

I have a more general question. I assume this class will be extended by a first party package?

In that case why isn't the two_dimentional_viewport.dart in that package instead of here?

visitor(child);
child = parentDataOf(child)._nextSibling;
}
_keepAliveBucket.values.forEach(visitor);
Copy link
Contributor

Choose a reason for hiding this comment

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

I am slightly surprised keptalive children are visited as well in this method. I did see the slivermultiboxadapter also does this, but I am not entirely sure why.

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 also am not aware of why. I traced it back to #11010 when keep alive as first added. I think of it being kind of similar to the Offstage widget. The child is kept alive without painting anything, without making the child available for hit testing, and without taking any room in the parent. However, Offstage children are still active: they can receive focus and have keyboard input directed to them.

_keepAliveBucket.values.forEach(visitor);
}

@override
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 visitChildrenForSemantics only visits children that are visible, how does it work if user trying to visit offscreen item (thus will cause scroll)?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Trying to remember... I think this was because ensureVisible/getOffsetToReveal is not yet implemented for RenderTwoDimensionalViewport. That is in #128812. Otherwise right now it would crash, so I think I remember putting the visible part in place to prevent a crash until it was properly implemented. I almost forgot about it, I added a note to the PR for when I get back to it.

Copy link
Contributor

Choose a reason for hiding this comment

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

Can you add a todo to here just to make sure we don't forget it once we implemented these methods?

}
// If the child in the bucket is not current child, that means someone has
// already moved and replaced current child, and we cannot remove this
// child.
Copy link
Contributor

Choose a reason for hiding this comment

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

I think this may only possible if the earlier movechild move another child into this place. In this case this child will be in _debugDanglingKeepAlives. Maybe add an assert check here?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

For this and the comment below, this matches almost exactly to the implementation in RenderSliverMultiBoxAdaptor, should the same changes be made there? I traced this back to #31978 for context.

return;
}
assert(_debugTrackOrphans(noLongerOrphan: child));
assert(_keepAliveBucket[childParentData.vicinity] == child);
Copy link
Contributor

Choose a reason for hiding this comment

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

if they are in _debugDanglingKeepAlives they won't be in here, no?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

They should be. Every child in _debugDanglingKeepAlives should be in the _keepAliveBucket, but not necessarily the other way around.

Copy link
Contributor

Choose a reason for hiding this comment

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

you are right, I misread the code


void _insertChild(RenderBox child, ChildVicinity slot) {
assert(_debugTrackOrphans(newOrphan: _children[slot]));
assert(!_keepAliveBucket.containsValue(child));
Copy link
Contributor

Choose a reason for hiding this comment

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

also check _debugDanglingKeepAlives ?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Should it be empty, or not contain the child? We do not have a similar check in RenderSliverMultiBoxAdaptor, should I add one there too? RenderTwoDimensionalViewport is like a cross between RenderSliverMultiBoxAdaptor and Viewport with two offsets, so I have been trying to match behavior as much as possible for consistency. :)

Copy link
Contributor

Choose a reason for hiding this comment

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

I think they should not contain the child because it should be removed from the _debugDanglingKeepAlives.

Given this is mirroring the 1D case, I am fine either you add both or not.

Can also think about extracting a mixin with generic slot type if things start to get repeated.

/// Returns the child for a given [ChildVicinity].
///
/// This method will build the child if it has not been already, or will reuse
/// it if it already exists.
Copy link
Contributor

Choose a reason for hiding this comment

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

This method can have more detail doc string. Maybe add some comment around when this method should be call (in layoutChildSequence I think?) and the childvicinity must be what is visible on the screen because this method will insert the child into active children list

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The children aren't required to be visible since they could be in the cache extent and are ok as active children there. I will add more docs though.

Copy link
Contributor

Choose a reason for hiding this comment

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

The comment seems not updated yet?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ah thank you!

Copy link
Contributor Author

@Piinks Piinks left a comment

Choose a reason for hiding this comment

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

Thanks for review! Good questions!

I assume this class will be extended by a first party package?

Yup! The first version is in flutter/packages#4536

In that case why isn't the two_dimensional_viewport.dart in that package instead of here?

A couple of reasons. The main one being that the 2D foundation must be in the framework due to some overrides of private API in Scrollable.
Additionally, there is a lot of scrolling functionality built-in to other widgets in the framework, and we want them all to work the same whether it is 1D or 2D.

ensureVisible is a good example, which supports default focus traversal. We would not want developers to have to call ensureVisible for 1D and then some other method for 2D, on top of the fact that several places in the framework call this and we want it to just work rather than having two different code paths.

Implementing the core functionality of 2D in the framework and providing base classes for developers to build on top of was the general idea. The 1P packages we are building on top of it in flutter/packages may move into the framework later, but we wanted to be able to iterate on them more rapidly from the package repo first.

visitor(child);
child = parentDataOf(child)._nextSibling;
}
_keepAliveBucket.values.forEach(visitor);
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 also am not aware of why. I traced it back to #11010 when keep alive as first added. I think of it being kind of similar to the Offstage widget. The child is kept alive without painting anything, without making the child available for hit testing, and without taking any room in the parent. However, Offstage children are still active: they can receive focus and have keyboard input directed to them.

_keepAliveBucket.values.forEach(visitor);
}

@override
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Trying to remember... I think this was because ensureVisible/getOffsetToReveal is not yet implemented for RenderTwoDimensionalViewport. That is in #128812. Otherwise right now it would crash, so I think I remember putting the visible part in place to prevent a crash until it was properly implemented. I almost forgot about it, I added a note to the PR for when I get back to it.

/// Returns the child for a given [ChildVicinity].
///
/// This method will build the child if it has not been already, or will reuse
/// it if it already exists.
Copy link
Contributor Author

Choose a reason for hiding this comment

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

The children aren't required to be visible since they could be in the cache extent and are ok as active children there. I will add more docs though.


void _insertChild(RenderBox child, ChildVicinity slot) {
assert(_debugTrackOrphans(newOrphan: _children[slot]));
assert(!_keepAliveBucket.containsValue(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.

Should it be empty, or not contain the child? We do not have a similar check in RenderSliverMultiBoxAdaptor, should I add one there too? RenderTwoDimensionalViewport is like a cross between RenderSliverMultiBoxAdaptor and Viewport with two offsets, so I have been trying to match behavior as much as possible for consistency. :)

}
// If the child in the bucket is not current child, that means someone has
// already moved and replaced current child, and we cannot remove this
// 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.

For this and the comment below, this matches almost exactly to the implementation in RenderSliverMultiBoxAdaptor, should the same changes be made there? I traced this back to #31978 for context.

return;
}
assert(_debugTrackOrphans(noLongerOrphan: child));
assert(_keepAliveBucket[childParentData.vicinity] == 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.

They should be. Every child in _debugDanglingKeepAlives should be in the _keepAliveBucket, but not necessarily the other way around.

@Piinks Piinks requested a review from chunhtai August 8, 2023 19:02
Copy link
Contributor

@chunhtai chunhtai left a comment

Choose a reason for hiding this comment

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

LGTM, just left some minor comments

_keepAliveBucket.values.forEach(visitor);
}

@override
Copy link
Contributor

Choose a reason for hiding this comment

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

Can you add a todo to here just to make sure we don't forget it once we implemented these methods?

/// Returns the child for a given [ChildVicinity].
///
/// This method will build the child if it has not been already, or will reuse
/// it if it already exists.
Copy link
Contributor

Choose a reason for hiding this comment

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

The comment seems not updated yet?

_childManager._buildChild(vicinity);
});
} else {
if (_keepAliveBucket.containsKey(vicinity)) {
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 check needed? I think you can just remove even if it is not in the map?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Oh right. Good call.


void _insertChild(RenderBox child, ChildVicinity slot) {
assert(_debugTrackOrphans(newOrphan: _children[slot]));
assert(!_keepAliveBucket.containsValue(child));
Copy link
Contributor

Choose a reason for hiding this comment

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

I think they should not contain the child because it should be removed from the _debugDanglingKeepAlives.

Given this is mirroring the 1D case, I am fine either you add both or not.

Can also think about extracting a mixin with generic slot type if things start to get repeated.

return;
}
assert(_debugTrackOrphans(noLongerOrphan: child));
assert(_keepAliveBucket[childParentData.vicinity] == child);
Copy link
Contributor

Choose a reason for hiding this comment

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

you are right, I misread the code

@Piinks Piinks added the autosubmit Merge PR when tree becomes green via auto submit App label Aug 9, 2023
@auto-submit auto-submit bot removed the autosubmit Merge PR when tree becomes green via auto submit App label Aug 9, 2023
@auto-submit
Copy link
Contributor

auto-submit bot commented Aug 9, 2023

auto label is removed for flutter/flutter/131641, due to - The status or check suite Linux web_tests_7_last has failed. Please fix the issues identified (or deflake) before re-applying this label.

@Piinks
Copy link
Contributor Author

Piinks commented Aug 9, 2023

Oh whoops I broke something

@Piinks
Copy link
Contributor Author

Piinks commented Aug 9, 2023

Yay for tests. 😍

@Piinks Piinks added the autosubmit Merge PR when tree becomes green via auto submit App label Aug 9, 2023
@auto-submit auto-submit bot merged commit 0c80ed6 into flutter:master Aug 10, 2023
engine-flutter-autoroll added a commit to engine-flutter-autoroll/packages that referenced this pull request Aug 10, 2023
engine-flutter-autoroll added a commit to engine-flutter-autoroll/packages that referenced this pull request Aug 10, 2023
engine-flutter-autoroll added a commit to engine-flutter-autoroll/packages that referenced this pull request Aug 10, 2023
engine-flutter-autoroll added a commit to engine-flutter-autoroll/packages that referenced this pull request Aug 10, 2023
engine-flutter-autoroll added a commit to engine-flutter-autoroll/packages that referenced this pull request Aug 10, 2023
engine-flutter-autoroll added a commit to engine-flutter-autoroll/packages that referenced this pull request Aug 10, 2023
engine-flutter-autoroll added a commit to engine-flutter-autoroll/packages that referenced this pull request Aug 10, 2023
engine-flutter-autoroll added a commit to engine-flutter-autoroll/packages that referenced this pull request Aug 10, 2023
engine-flutter-autoroll added a commit to engine-flutter-autoroll/packages that referenced this pull request Nov 15, 2023
@Piinks Piinks deleted the keepAlives2D branch March 14, 2025 17:13
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 c: new feature Nothing broken; request for a new capability f: scrolling Viewports, list views, slivers, etc. framework flutter/packages/flutter repository. See also f: labels.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2D Follow up: Keep alive support

2 participants