Reland "Fix DraggableScrollableSheet rebuilding during drag" (#112479)
diff --git a/packages/flutter/lib/src/widgets/draggable_scrollable_sheet.dart b/packages/flutter/lib/src/widgets/draggable_scrollable_sheet.dart
index 0a6e647..a0658cc 100644
--- a/packages/flutter/lib/src/widgets/draggable_scrollable_sheet.dart
+++ b/packages/flutter/lib/src/widgets/draggable_scrollable_sheet.dart
@@ -21,6 +21,7 @@
import 'scroll_position.dart';
import 'scroll_position_with_single_context.dart';
import 'scroll_simulation.dart';
+import 'value_listenable_builder.dart';
/// The signature of a method that provides a [BuildContext] and
/// [ScrollController] for building a widget that may overflow the draggable
@@ -488,7 +489,6 @@
required this.snap,
required this.snapSizes,
required this.initialSize,
- required this.onSizeChanged,
this.snapAnimationDuration,
ValueNotifier<double>? currentSize,
bool? hasDragged,
@@ -500,8 +500,7 @@
assert(maxSize <= 1),
assert(minSize <= initialSize),
assert(initialSize <= maxSize),
- _currentSize = (currentSize ?? ValueNotifier<double>(initialSize))
- ..addListener(onSizeChanged),
+ _currentSize = currentSize ?? ValueNotifier<double>(initialSize),
availablePixels = double.infinity,
hasDragged = hasDragged ?? false,
hasChanged = hasChanged ?? false;
@@ -515,7 +514,6 @@
final Duration? snapAnimationDuration;
final double initialSize;
final ValueNotifier<double> _currentSize;
- final VoidCallback onSizeChanged;
double availablePixels;
// Used to disable snapping until the user has dragged on the sheet.
@@ -595,17 +593,12 @@
return size / maxSize * availablePixels;
}
- void dispose() {
- _currentSize.removeListener(onSizeChanged);
- }
-
_DraggableSheetExtent copyWith({
required double minSize,
required double maxSize,
required bool snap,
required List<double> snapSizes,
required double initialSize,
- required VoidCallback onSizeChanged,
Duration? snapAnimationDuration,
}) {
return _DraggableSheetExtent(
@@ -615,7 +608,6 @@
snapSizes: snapSizes,
snapAnimationDuration: snapAnimationDuration,
initialSize: initialSize,
- onSizeChanged: onSizeChanged,
// Set the current size to the possibly updated initial size if the sheet
// hasn't changed yet.
currentSize: ValueNotifier<double>(hasChanged
@@ -641,7 +633,6 @@
snapSizes: _impliedSnapSizes(),
snapAnimationDuration: widget.snapAnimationDuration,
initialSize: widget.initialChildSize,
- onSizeChanged: _setExtent,
);
_scrollController = _DraggableScrollableSheetScrollController(extent: _extent);
widget.controller?._attach(_scrollController);
@@ -683,24 +674,22 @@
}
}
- void _setExtent() {
- setState(() {
- // _extent has been updated when this is called.
- });
- }
-
@override
Widget build(BuildContext context) {
- return LayoutBuilder(
- builder: (BuildContext context, BoxConstraints constraints) {
- _extent.availablePixels = widget.maxChildSize * constraints.biggest.height;
- final Widget sheet = FractionallySizedBox(
- heightFactor: _extent.currentSize,
- alignment: Alignment.bottomCenter,
- child: widget.builder(context, _scrollController),
- );
- return widget.expand ? SizedBox.expand(child: sheet) : sheet;
- },
+ return ValueListenableBuilder<double>(
+ valueListenable: _extent._currentSize,
+ builder: (BuildContext context, double currentSize, Widget? child) => LayoutBuilder(
+ builder: (BuildContext context, BoxConstraints constraints) {
+ _extent.availablePixels = widget.maxChildSize * constraints.biggest.height;
+ final Widget sheet = FractionallySizedBox(
+ heightFactor: currentSize,
+ alignment: Alignment.bottomCenter,
+ child: child,
+ );
+ return widget.expand ? SizedBox.expand(child: sheet) : sheet;
+ },
+ ),
+ child: widget.builder(context, _scrollController),
);
}
@@ -708,13 +697,11 @@
void dispose() {
widget.controller?._detach();
_scrollController.dispose();
- _extent.dispose();
super.dispose();
}
void _replaceExtent(covariant DraggableScrollableSheet oldWidget) {
final _DraggableSheetExtent previousExtent = _extent;
- _extent.dispose();
_extent = _extent.copyWith(
minSize: widget.minChildSize,
maxSize: widget.maxChildSize,
@@ -722,7 +709,6 @@
snapSizes: _impliedSnapSizes(),
snapAnimationDuration: widget.snapAnimationDuration,
initialSize: widget.initialChildSize,
- onSizeChanged: _setExtent,
);
// Modify the existing scroll controller instead of replacing it so that
// developers listening to the controller do not have to rebuild their listeners.
diff --git a/packages/flutter/test/widgets/draggable_scrollable_sheet_test.dart b/packages/flutter/test/widgets/draggable_scrollable_sheet_test.dart
index 6ae2fc6..e7f0752 100644
--- a/packages/flutter/test/widgets/draggable_scrollable_sheet_test.dart
+++ b/packages/flutter/test/widgets/draggable_scrollable_sheet_test.dart
@@ -1465,4 +1465,51 @@
closeTo(.6, precisionErrorTolerance),
);
});
+
+ testWidgets('DraggableScrollableSheet should not rebuild every frame while dragging', (WidgetTester tester) async {
+ // Regression test for https://github.com/flutter/flutter/issues/67219
+ int buildCount = 0;
+ await tester.pumpWidget(MaterialApp(
+ home: StatefulBuilder(
+ builder: (BuildContext context, StateSetter setState) => Scaffold(
+ body: DraggableScrollableSheet(
+ initialChildSize: 0.25,
+ snap: true,
+ snapSizes: const <double>[0.25, 0.5, 1.0],
+ builder: (BuildContext context, ScrollController scrollController) {
+ buildCount++;
+ return ListView(
+ controller: scrollController,
+ children: <Widget>[
+ const Text('Drag me!'),
+ ElevatedButton(
+ onPressed: () => setState(() {}),
+ child: const Text('Rebuild'),
+ ),
+ Container(
+ height: 10000,
+ color: Colors.blue,
+ ),
+ ],
+ );
+ },
+ ),
+ ),
+ ),
+ ));
+
+ expect(buildCount, 1);
+
+ await tester.fling(find.text('Drag me!'), const Offset(0, -300), 300);
+ await tester.pumpAndSettle();
+
+ // No need to rebuild the scrollable sheet, as only position has changed.
+ expect(buildCount, 1);
+
+ await tester.tap(find.text('Rebuild'));
+ await tester.pump();
+
+ // DraggableScrollableSheet has rebuilt, so expect the builder to be called.
+ expect(buildCount, 2);
+ });
}