Skip to content

Conversation

@dnfield
Copy link
Contributor

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

));
expect(buildCount, 1);
await tester.flingFrom(const Offset(0, 325), const Offset(0, -325), 200);
expect(buildCount, 1);
Copy link
Contributor Author

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

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?

Copy link
Member

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?

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 think I can address this with didUpdateWidget - will push something momentarily.

Copy link
Member

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

Copy link
Contributor Author

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.

Copy link
Member

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

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
Contributor Author

dnfield commented Nov 5, 2020

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

@dnfield
Copy link
Contributor Author

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

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.

Copy link
Member

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?

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 think it's probably still fine

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

_extent = _DraggableSheetExtent(
Copy link
Member

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?

Copy link
Contributor Author

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

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) {
Copy link
Member

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.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done

@goderbauer goderbauer added the framework flutter/packages/flutter repository. See also f: labels. label Nov 6, 2020
class _DraggableScrollableSheetState extends State<DraggableScrollableSheet> {
late _DraggableScrollableSheetScrollController _scrollController;
late _DraggableSheetExtent _extent;
// The child only gets rebuilt when dependencies change. Otherwise, excessive
Copy link
Member

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.

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

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

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

Copy link
Contributor Author

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

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?

Copy link
Contributor Author

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

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

Choose a reason for hiding this comment

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

Sounds good.

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

@fluttergithubbot fluttergithubbot merged commit 16dce76 into flutter:master Nov 6, 2020
@SiyrisSoul
Copy link
Contributor

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

@dnfield
Copy link
Contributor Author

dnfield commented Nov 13, 2020

Hmm. We probably need something for reassemble.

@dnfield
Copy link
Contributor Author

dnfield commented Nov 13, 2020

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

@Hixie
Copy link
Contributor

Hixie commented Nov 13, 2020

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 (??=), that way you know the timing is going to be appropriate.

@dnfield
Copy link
Contributor Author

dnfield commented Nov 13, 2020

There's something strange going on here still.

@SiyrisSoul
Copy link
Contributor

I'm going to mess around with this today. Any progress been made in the meantime?

@dnfield
Copy link
Contributor Author

dnfield commented Nov 15, 2020

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

@SiyrisSoul
Copy link
Contributor

@dnfield Can we move _updateExtent() from didUpdateWidget()? I'm honestly not entirely sure what calling it there is supposed to do. At least in my use case removing it made everything seemingly work as intended. I also tried moving it into didChangeDependencies() above setting the _child with no issues, but again not sure what case exactly I should be testing for.

@SiyrisSoul
Copy link
Contributor

Any progress on this? I haven't yet noticed anything not working after the changes I mentioned above.

@dnfield
Copy link
Contributor Author

dnfield commented Nov 19, 2020

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.

@dnfield
Copy link
Contributor Author

dnfield commented Nov 19, 2020

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.

@dnfield
Copy link
Contributor Author

dnfield commented Nov 19, 2020

Unlikely to get to it in the next two weeks I mean :)

@SiyrisSoul
Copy link
Contributor

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

dnfield added a commit that referenced this pull request Dec 1, 2020
dnfield added a commit that referenced this pull request Dec 2, 2020
@gabrieldelorean
Copy link

Any progress on this?

@dnfield
Copy link
Contributor Author

dnfield commented Jan 21, 2021

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.

@agiletechie
Copy link

is this being worked on?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

framework flutter/packages/flutter repository. See also f: labels.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

DraggableScrollableSheet is rebuilding the screen while dragging

7 participants