diff --git a/bin/internal/engine.version b/bin/internal/engine.version index b9b5db1343a2c..d13baebf32373 100644 --- a/bin/internal/engine.version +++ b/bin/internal/engine.version @@ -1 +1 @@ -8aace64d19fcf6330ceccdc6f4a2a2726064c47a +cd78221a88ae704ae94323344cc793d6ecc20e9c diff --git a/packages/flutter/lib/src/widgets/draggable_scrollable_sheet.dart b/packages/flutter/lib/src/widgets/draggable_scrollable_sheet.dart index 0ccae36798f3d..a53437444a3ca 100644 --- a/packages/flutter/lib/src/widgets/draggable_scrollable_sheet.dart +++ b/packages/flutter/lib/src/widgets/draggable_scrollable_sheet.dart @@ -52,6 +52,7 @@ typedef ScrollableWidgetBuilder = Widget Function( /// constraints provided to an attached sheet change. class DraggableScrollableController extends ChangeNotifier { _DraggableScrollableSheetScrollController? _attachedController; + final Set _animationControllers = {}; /// Get the current size (as a fraction of the parent height) of the attached sheet. double get size { @@ -115,6 +116,7 @@ class DraggableScrollableController extends ChangeNotifier { vsync: _attachedController!.position.context.vsync, value: _attachedController!.extent.currentSize, ); + _animationControllers.add(animationController); _attachedController!.position.goIdle(); // This disables any snapping until the next user interaction with the sheet. _attachedController!.extent.hasDragged = false; @@ -175,6 +177,7 @@ class DraggableScrollableController extends ChangeNotifier { assert(_attachedController == null, 'Draggable scrollable controller is already attached to a sheet.'); _attachedController = scrollController; _attachedController!.extent._currentSize.addListener(notifyListeners); + _attachedController!.onPositionDetached = _disposeAnimationControllers; } void _onExtentReplaced(_DraggableSheetExtent previousExtent) { @@ -193,6 +196,13 @@ class DraggableScrollableController extends ChangeNotifier { _attachedController?.extent._currentSize.removeListener(notifyListeners); _attachedController = null; } + + void _disposeAnimationControllers() { + for (final AnimationController animationController in _animationControllers) { + animationController.dispose(); + } + _animationControllers.clear(); + } } /// A container for a [Scrollable] that responds to drag gestures by resizing @@ -724,6 +734,7 @@ class _DraggableScrollableSheetScrollController extends ScrollController { }) : assert(extent != null); _DraggableSheetExtent extent; + VoidCallback? onPositionDetached; @override _DraggableScrollableSheetScrollPosition createScrollPosition( @@ -764,6 +775,12 @@ class _DraggableScrollableSheetScrollController extends ScrollController { } extent.updateSize(extent.initialSize, position.context.notificationContext!); } + + @override + void detach(ScrollPosition position) { + onPositionDetached?.call(); + super.detach(position); + } } /// A scroll position that manages scroll activities for diff --git a/packages/flutter/test/widgets/draggable_scrollable_sheet_test.dart b/packages/flutter/test/widgets/draggable_scrollable_sheet_test.dart index bfd65190b9dc9..28375d25328c2 100644 --- a/packages/flutter/test/widgets/draggable_scrollable_sheet_test.dart +++ b/packages/flutter/test/widgets/draggable_scrollable_sheet_test.dart @@ -1209,4 +1209,19 @@ void main() { expect(controller.isAttached, true); expect(controller.size, isNotNull); }); + + testWidgets('DraggableScrollableController.animateTo should not leak Ticker', (WidgetTester tester) async { + // Regression test for https://github.com/flutter/flutter/issues/102483 + final DraggableScrollableController controller = DraggableScrollableController(); + await tester.pumpWidget(_boilerplate(() {}, controller: controller)); + + controller.animateTo(0.0, curve: Curves.linear, duration: const Duration(milliseconds: 200)); + await tester.pump(); + + // Dispose the DraggableScrollableSheet + await tester.pumpWidget(const SizedBox.shrink()); + // Controller should be detached and no exception should be thrown + expect(controller.isAttached, false); + expect(tester.takeException(), isNull); + }); }