-
Notifications
You must be signed in to change notification settings - Fork 29.7k
Fix excessive rebuilds of DSS #69724
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
| )); | ||
| expect(buildCount, 1); | ||
| await tester.flingFrom(const Offset(0, 325), const Offset(0, -325), 200); | ||
| expect(buildCount, 1); |
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.
Before this change, the buildCount was coming out to 48.
| } | ||
| _extent._currentExtent.value = _extent.initialExtent; | ||
| } | ||
| _child = widget.builder(context, _scrollController); |
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 if the DraggableScrollableSheet is given a new builder during its lifetime?
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.
Unrelated: Looks like changes to minChildSize and maxChildSize are also not respected, which seems like another bug?
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 think I can address this with didUpdateWidget - will push something momentarily.
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 seem to recall that a builder is best called on every build (that's what the canonical Builder widget does at least). I don't remember why, though. /cc @Hixie
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.
In this case, that's too much - so I'm trying to cache the output of that builder to avoid rebuilding it whenever the sheet is dragged up or down.
This is simlar to how we don't rebuild every child sliver when a listview scrolls.
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.
|
Conceptually, could you have the DraggableScrollableSheet build an intermediate widget that takes the prebuild child widget. The intermediate widget then contains the layoutbuilder and the logic to frequently rebuild, but since it is configured with a prebuild child, the child wouldn't build again? Something like: class DraggableScrollableSheet extends StatelessWidget {
DraggableScrollableSheet(
// ...
this.builder,
);
// ..
Widget build(BuildCOntext context) {
return _Intermediate(
//..
child: builder(context),
);
}_Intermediate is basically what DraggableScrollableSheet is today with a child instead of a builder param. Only difficulty: We'd have to move ownership of the scroll controller out of the _Intermediate widget to this new DraggableScrollableSheet since the builder needs it as argument. That doesn't seem impossible, though? |
|
Can't I just update the child in |
|
@goderbauer added some tests that failed based on your suggestion, fixed them with didUpdateWidget - nice catch on the min/max/initial extent too. PTAL. |
| _child = widget.builder(context, _scrollController); | ||
| } | ||
|
|
||
| _extent = _DraggableSheetExtent( |
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 cheap to create, so it seems silly to check the min/max/initial sizes for changes.
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 still true even with us now creating a new _scrollController as well, which will cause other widgets to move their listeners over to the new one?
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 think it's probably still fine
| _child = widget.builder(context, _scrollController); | ||
| } | ||
|
|
||
| _extent = _DraggableSheetExtent( |
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.
How is the new extend wired up to the _DraggableScrollableSheetScrollController?
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.
Fixed
| } | ||
| _extent._currentExtent.value = _extent.initialExtent; | ||
| } | ||
| _child = widget.builder(context, _scrollController); |
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 seem to recall that a builder is best called on every build (that's what the canonical Builder widget does at least). I don't remember why, though. /cc @Hixie
| @override | ||
| void didUpdateWidget(DraggableScrollableSheet oldWidget) { | ||
| super.didUpdateWidget(oldWidget); | ||
| if (oldWidget.builder != widget.builder) { |
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.
If anything, I think you need to call the builder even if the closure has the same identity. For example, if somebody passes in a tear-off function of a State object, you always get the same builder object, but values closured into that builder may have changed.
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
| class _DraggableScrollableSheetState extends State<DraggableScrollableSheet> { | ||
| late _DraggableScrollableSheetScrollController _scrollController; | ||
| late _DraggableSheetExtent _extent; | ||
| // The child only gets rebuilt when dependencies change. Otherwise, excessive |
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.
nit: comment is out of date, also rebuilds on didUpdateWidget.
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.
Updated
| @override | ||
| void didUpdateWidget(DraggableScrollableSheet oldWidget) { | ||
| super.didUpdateWidget(oldWidget); | ||
| _child = widget.builder(context, _scrollController); |
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 add a quick comment here reminding us in the future why we have to call this unconditionally (and not just when builder changes)?
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.
| void didUpdateWidget(DraggableScrollableSheet oldWidget) { | ||
| super.didUpdateWidget(oldWidget); | ||
| _child = widget.builder(context, _scrollController); | ||
| _updateExtent(); |
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 think _updateExtent has to run before we execute the builder since it instantiates a new scrollController that's used by the builder?
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.
| _child = widget.builder(context, _scrollController); | ||
| } | ||
|
|
||
| _extent = _DraggableSheetExtent( |
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 still true even with us now creating a new _scrollController as well, which will cause other widgets to move their listeners over to the new one?
| } | ||
| _extent._currentExtent.value = _extent.initialExtent; | ||
| } | ||
| _child = widget.builder(context, _scrollController); |
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.
goderbauer
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
|
@dnfield With this change I'm getting inconsistent behavior between builds of my app. It works perfectly when first implementing but after a hot restart or full rebuild it doesn't work and only seems to register the scroll controller for the content inside the sheet. If I change the widget tree by for example adding a container around the DSS then it will work again just once, and again stop being draggable after any restarts or refreshes. |
|
Hmm. We probably need something for reassemble. |
|
@TutOsirisOm - if you add something like this does it work: diff --git a/packages/flutter/lib/src/widgets/draggable_scrollable_sheet.dart b/packages/flutter/lib/src/widgets/draggable_scrollable_sheet.dart
index 9303c98c78..68fa9608c7 100644
--- a/packages/flutter/lib/src/widgets/draggable_scrollable_sheet.dart
+++ b/packages/flutter/lib/src/widgets/draggable_scrollable_sheet.dart
@@ -330,6 +330,16 @@ class _DraggableScrollableSheetState extends State<DraggableScrollableSheet> {
_child = widget.builder(context, _scrollController);
}
+ @override
+ void reassemble() {
+ super.reassemble();
+ _updateExtent();
+ // Call this unconditionally - the closure may not change even though it
+ // refers to things outside of its identity, e.g. a tearoff from state that
+ // has an `if (stateVariable)`.
+ _child = widget.builder(context, _scrollController);
+ }
+
void _updateExtent() {
_extent = _DraggableSheetExtent(
minExtent: widget.minChildSize, |
|
Calling it from reassemble itself is probably sketchy (reassemble is called before the build phase). Better to do something like set _child to null and have the build method call the builder if it's null ( |
|
There's something strange going on here still. |
|
I'm going to mess around with this today. Any progress been made in the meantime? |
|
I started looking Friday afternoon but didn't make a lot of progress. I can look more tomorrow if you dont get to it first |
|
@dnfield Can we move |
|
Any progress on this? I haven't yet noticed anything not working after the changes I mentioned above. |
|
I haven't had a chance to get back to this yet. Sorry. I may be able to today, but I have a couple other higher priority items to make progress on first. |
|
It is unlikely that I will be able to get to this. If we need to revert this change until we can fix this I think that's fine. |
|
Unlikely to get to it in the next two weeks I mean :) |
|
That's fine, in the meantime I'll look into it some more but as is the changes I made earlier are working as intended for me. :) |
|
Any progress on this? |
|
I have not had time to look at this again and am not sure when I'll get back to it. I still suspect something like this fix can work, but it needs to address hot restart issues as well as something wrong with how the scroll controller was or wasn't getting recreated. |
|
is this being worked on? |
Description
DraggableScrollableSheet internally ends up calling the builder function every time the sheet scrolls - this is really unfortunate when, for example, it contains a sizable ListView that gets rebuilt every time the sheet slides up or down one pixel.
To resolve this, move the call of the builder function to
didChangeDependenciesso it's only called when our position in the tree changes, and reuse the cached child inbuild.Related Issues
Fixes #67219
Tests
Assert that the builder is only called once when dependencies do not change and the sheet is dragged.
This is not a breaking change.