Skip to content
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

Fix excessive rebuilds of DSS #69724

Merged
merged 5 commits into from Nov 6, 2020
Merged

Conversation

@dnfield
Copy link
Member

@dnfield dnfield commented Nov 3, 2020

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 didChangeDependencies so it's only called when our position in the tree changes, and reuse the cached child in build.

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.

@dnfield dnfield force-pushed the dnfield:drag_build branch from 8e243d5 to 3567e83 Nov 3, 2020
));
expect(buildCount, 1);
await tester.flingFrom(const Offset(0, 325), const Offset(0, -325), 200);
expect(buildCount, 1);

This comment has been minimized.

@dnfield

dnfield Nov 3, 2020
Author Member

Before this change, the buildCount was coming out to 48.

@@ -317,13 +323,13 @@ class _DraggableScrollableSheetState extends State<DraggableScrollableSheet> {
}
_extent._currentExtent.value = _extent.initialExtent;
}
_child = widget.builder(context, _scrollController);

This comment has been minimized.

@goderbauer

goderbauer Nov 4, 2020
Member

What if the DraggableScrollableSheet is given a new builder during its lifetime?

This comment has been minimized.

@goderbauer

goderbauer Nov 4, 2020
Member

Unrelated: Looks like changes to minChildSize and maxChildSize are also not respected, which seems like another bug?

This comment has been minimized.

@dnfield

dnfield Nov 5, 2020
Author Member

I think I can address this with didUpdateWidget - will push something momentarily.

This comment has been minimized.

@goderbauer

goderbauer Nov 5, 2020
Member

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

This comment has been minimized.

@dnfield

dnfield Nov 5, 2020
Author Member

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.

This comment has been minimized.

@goderbauer

goderbauer Nov 6, 2020
Member

Sounds good.

@goderbauer
Copy link
Member

@goderbauer goderbauer commented Nov 4, 2020

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?

@dnfield
Copy link
Member Author

@dnfield dnfield commented Nov 5, 2020

Can't I just update the child in didUpdateWidget and it shoul dbe fine?

@dnfield
Copy link
Member Author

@dnfield dnfield commented Nov 5, 2020

@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(

This comment has been minimized.

@dnfield

dnfield Nov 5, 2020
Author Member

This is cheap to create, so it seems silly to check the min/max/initial sizes for changes.

This comment has been minimized.

@goderbauer

goderbauer Nov 6, 2020
Member

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?

This comment has been minimized.

@dnfield

dnfield Nov 6, 2020
Author Member

I think it's probably still fine

_child = widget.builder(context, _scrollController);
}

_extent = _DraggableSheetExtent(

This comment has been minimized.

@goderbauer

goderbauer Nov 5, 2020
Member

How is the new extend wired up to the _DraggableScrollableSheetScrollController?

This comment has been minimized.

@dnfield

dnfield Nov 5, 2020
Author Member

Fixed

@@ -317,13 +323,13 @@ class _DraggableScrollableSheetState extends State<DraggableScrollableSheet> {
}
_extent._currentExtent.value = _extent.initialExtent;
}
_child = widget.builder(context, _scrollController);

This comment has been minimized.

@goderbauer

goderbauer Nov 5, 2020
Member

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

This comment has been minimized.

@goderbauer

goderbauer Nov 5, 2020
Member

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.

This comment has been minimized.

@dnfield

dnfield Nov 5, 2020
Author Member

Done

@goderbauer goderbauer added the framework label Nov 6, 2020
@@ -288,17 +288,17 @@ class _DraggableSheetExtent {
class _DraggableScrollableSheetState extends State<DraggableScrollableSheet> {
late _DraggableScrollableSheetScrollController _scrollController;
late _DraggableSheetExtent _extent;
// The child only gets rebuilt when dependencies change. Otherwise, excessive

This comment has been minimized.

@goderbauer

goderbauer Nov 6, 2020
Member

nit: comment is out of date, also rebuilds on didUpdateWidget.

This comment has been minimized.

@dnfield

dnfield Nov 6, 2020
Author Member

Updated

@override
void didUpdateWidget(DraggableScrollableSheet oldWidget) {
super.didUpdateWidget(oldWidget);
_child = widget.builder(context, _scrollController);

This comment has been minimized.

@goderbauer

goderbauer Nov 6, 2020
Member

Maybe add a quick comment here reminding us in the future why we have to call this unconditionally (and not just when builder changes)?

This comment has been minimized.

@dnfield

dnfield Nov 6, 2020
Author Member

Done.

void didUpdateWidget(DraggableScrollableSheet oldWidget) {
super.didUpdateWidget(oldWidget);
_child = widget.builder(context, _scrollController);
_updateExtent();

This comment has been minimized.

@goderbauer

goderbauer Nov 6, 2020
Member

I think _updateExtent has to run before we execute the builder since it instantiates a new scrollController that's used by the builder?

This comment has been minimized.

@dnfield

dnfield Nov 6, 2020
Author Member

Done.

_child = widget.builder(context, _scrollController);
}

_extent = _DraggableSheetExtent(

This comment has been minimized.

@goderbauer

goderbauer Nov 6, 2020
Member

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?

@@ -317,13 +323,13 @@ class _DraggableScrollableSheetState extends State<DraggableScrollableSheet> {
}
_extent._currentExtent.value = _extent.initialExtent;
}
_child = widget.builder(context, _scrollController);

This comment has been minimized.

@goderbauer

goderbauer Nov 6, 2020
Member

Sounds good.

Copy link
Member

@goderbauer goderbauer left a comment

LGTM

@fluttergithubbot fluttergithubbot merged commit 16dce76 into flutter:master Nov 6, 2020
36 checks passed
36 checks passed
Google testing Google testing passed!
Details
Linux analyze
Details
Linux build_gallery
Details
Linux build_tests
Details
Linux customer_testing
Details
Linux docs
Details
Linux firebase_abstract_method_smoke_test
Details
Linux firebase_android_embedding_v2_smoke_test
Details
Linux firebase_release_smoke_test
Details
Linux framework_tests
Details
Linux fuchsia_precache
Details
Linux web_e2e_test
Details
Linux web_integration_tests
Details
Linux web_long_running_tests
Details
Linux web_smoke_test
Details
Linux web_tests
Details
Mac build_gallery
Details
Mac build_tests
Details
Mac customer_testing
Details
Mac framework_tests
Details
WIP Ready for review
Details
Windows build_tests
Details
Windows customer_testing
Details
Windows framework_tests
Details
Windows tool_tests
Details
analyze-linux Task Summary
Details
cla/google All necessary CLAs are signed
customer_testing-linux Task Summary
Details
docs-linux Task Summary
Details
flutter-build
Details
flutter-gold All golden file tests have passed.
Details
framework_tests-libraries-linux Task Summary
Details
framework_tests-misc-linux Task Summary
Details
framework_tests-widgets-linux Task Summary
Details
web_integration_tests Task Summary
Details
web_smoke_test Task Summary
Details
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Linked issues

Successfully merging this pull request may close these issues.

3 participants
You can’t perform that action at this time.