diff --git a/packages/flutter/lib/src/widgets/animated_scroll_view.dart b/packages/flutter/lib/src/widgets/animated_scroll_view.dart index 4d03aec22b2..7926fd5bd51 100644 --- a/packages/flutter/lib/src/widgets/animated_scroll_view.dart +++ b/packages/flutter/lib/src/widgets/animated_scroll_view.dart @@ -920,7 +920,9 @@ const Duration _kDuration = Duration(milliseconds: 300); // Incoming and outgoing animated items. class _ActiveItem implements Comparable<_ActiveItem> { _ActiveItem.incoming(this.controller, this.itemIndex) : removedItemBuilder = null; + _ActiveItem.outgoing(this.controller, this.itemIndex, this.removedItemBuilder); + _ActiveItem.index(this.itemIndex) : controller = null, removedItemBuilder = null; final AnimationController? controller; @@ -1436,10 +1438,14 @@ abstract class _SliverAnimatedMultiBoxAdaptorState= 0; i--) { + assert(_itemsCount >= 0); + assert(_itemsCount - _outgoingItems.length >= 0); + final int visibleItemCount = _itemsCount - _outgoingItems.length; + for (int i = visibleItemCount - 1; i >= 0; i--) { removeItem(i, builder, duration: duration); } } diff --git a/packages/flutter/test/widgets/animated_list_test.dart b/packages/flutter/test/widgets/animated_list_test.dart index f02e4704adb..1b7fccecc9d 100644 --- a/packages/flutter/test/widgets/animated_list_test.dart +++ b/packages/flutter/test/widgets/animated_list_test.dart @@ -107,6 +107,73 @@ void main() { expect(find.text('removing item'), findsNothing); }); + testWidgets('AnimatedList should safely execute removeAllItems during long removal of one item', ( + WidgetTester tester, + ) async { + Widget builder(BuildContext context, int index, Animation animation) { + return SizedBox(height: 100.0, child: Center(child: Text('item $index'))); + } + + final GlobalKey listKey = GlobalKey(); + + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: AnimatedList(key: listKey, initialItemCount: 2, itemBuilder: builder), + ), + ); + + // Check that one AnimatedList with 2 items (item 0, item 1). + expect( + find.byWidgetPredicate((Widget widget) { + return widget is SliverAnimatedList && + widget.initialItemCount == 2 && + widget.itemBuilder == builder; + }), + findsOneWidget, + ); + expect(find.byType(Text), findsExactly(2)); + expect(find.text('item 0'), findsOne); + expect(find.text('item 1'), findsOne); + + // Insert 1 item and check state (item 0, item 1, item 2). + listKey.currentState!.insertItem(0, duration: const Duration(milliseconds: 200)); + await tester.pump(const Duration(milliseconds: 50)); + expect(find.byType(Text), findsExactly(3)); + expect(find.text('item 0'), findsOne); + expect(find.text('item 1'), findsOne); + expect(find.text('item 2'), findsOne); + + // Removing item 2 and check state (item 0, item 1, removing item 2). + listKey.currentState!.removeItem(2, (BuildContext context, Animation animation) { + return const SizedBox(height: 100.0, child: Center(child: Text('removing item 2'))); + }, duration: const Duration(milliseconds: 200)); + await tester.pump(const Duration(milliseconds: 50)); + expect(find.byType(Text), findsExactly(3)); + expect(find.text('item 0'), findsOne); + expect(find.text('item 1'), findsOne); + expect(find.text('removing item 2'), findsOne); + expect(find.text('item 2'), findsNothing); + + // Call removeAllItems and check state (removing all items, removing all items, removing item 2). + listKey.currentState!.removeAllItems((BuildContext context, Animation animation) { + return const SizedBox(height: 100.0, child: Center(child: Text('removing all items'))); + }, duration: const Duration(milliseconds: 100)); + await tester.pump(const Duration(milliseconds: 50)); + expect(find.byType(Text), findsExactly(3)); + expect(find.text('removing all items'), findsExactly(2)); + expect(find.text('removing item 2'), findsWidgets); + expect(find.text('item 0'), findsNothing); + expect(find.text('item 1'), findsNothing); + expect(find.text('item 2'), findsNothing); + + // After animation is done completed, list should be empty. + await tester.pumpAndSettle(); + expect(find.byType(Text), findsNothing); + expect(find.text('removing one item'), findsNothing); + expect(find.text('removing all items'), findsNothing); + }); + group('SliverAnimatedList', () { testWidgets('initialItemCount', (WidgetTester tester) async { final Map> animations = >{};