Added "insertAll" and "removeAll" methods to AnimatedList (#115545)

* Added "insertAll" and "removeAll" method to AnimatedList

* Fixed doc

* Changes in documentation asked by reviewwer

* Removed unnecessary asserts.

* Doc changes asked by reviewer.

* Doc changes.

---------

Co-authored-by: Rashid Khabeer <rkhabeer84@gmail.com>
diff --git a/packages/flutter/lib/src/widgets/animated_scroll_view.dart b/packages/flutter/lib/src/widgets/animated_scroll_view.dart
index 7938d78..9df6301 100644
--- a/packages/flutter/lib/src/widgets/animated_scroll_view.dart
+++ b/packages/flutter/lib/src/widgets/animated_scroll_view.dart
@@ -128,6 +128,10 @@
 /// animation is passed to [AnimatedList.itemBuilder] whenever the item's widget
 /// is needed.
 ///
+/// When multiple items are inserted with [insertAllItems] an animation begins running.
+/// The animation is passed to [AnimatedList.itemBuilder] whenever the item's widget
+/// is needed.
+///
 /// When an item is removed with [removeItem] its animation is reversed.
 /// The removed item's animation is passed to the [removeItem] builder
 /// parameter.
@@ -486,6 +490,13 @@
     _sliverAnimatedMultiBoxKey.currentState!.insertItem(index, duration: duration);
   }
 
+  /// Insert multiple items at [index] and start an animation that will be passed
+  /// to [AnimatedGrid.itemBuilder] or [AnimatedList.itemBuilder] when the items
+  /// are visible.
+  void insertAllItems(int index, int length, { Duration duration = _kDuration, bool isAsync = false }) {
+    _sliverAnimatedMultiBoxKey.currentState!.insertAllItems(index, length, duration: duration);
+  }
+
   /// Remove the item at `index` and start an animation that will be passed to
   /// `builder` when the item is visible.
   ///
@@ -506,6 +517,19 @@
     _sliverAnimatedMultiBoxKey.currentState!.removeItem(index, builder, duration: duration);
   }
 
+  /// Remove all the items and start an animation that will be passed to
+  /// `builder` when the items are visible.
+  ///
+  /// Items are removed immediately. However, the
+  /// items will still appear for `duration`, and during that time
+  /// `builder` must construct its widget as needed.
+  ///
+  /// This method's semantics are the same as Dart's [List.clear] method: it
+  /// removes all the items in the list.
+  void removeAllItems(AnimatedRemovedItemBuilder builder, { Duration duration = _kDuration }) {
+    _sliverAnimatedMultiBoxKey.currentState!.removeAllItems(builder, duration: duration);
+  }
+
   Widget _wrap(Widget sliver) {
     return CustomScrollView(
       scrollDirection: widget.scrollDirection,
@@ -1046,6 +1070,15 @@
     });
   }
 
+  /// Insert multiple items at [index] and start an animation that will be passed
+  /// to [AnimatedGrid.itemBuilder] or [AnimatedList.itemBuilder] when the items
+  /// are visible.
+  void insertAllItems(int index, int length, { Duration duration = _kDuration }) {
+    for (int i = 0; i < length; i++) {
+      insertItem(index + i, duration: duration);
+    }
+  }
+
   /// Remove the item at [index] and start an animation that will be passed
   /// to [builder] when the item is visible.
   ///
@@ -1094,4 +1127,19 @@
       setState(() => _itemsCount -= 1);
     });
   }
+
+  /// Remove all the items and start an animation that will be passed to
+  /// `builder` when the items are visible.
+  ///
+  /// Items are removed immediately. However, the
+  /// items will still appear for `duration` and during that time
+  /// `builder` must construct its widget as needed.
+  ///
+  /// This method's semantics are the same as Dart's [List.clear] method: it
+  /// removes all the items in the list.
+  void removeAllItems(AnimatedRemovedItemBuilder builder, { Duration duration = _kDuration }) {
+    for(int i = _itemsCount - 1 ; i >= 0; i--) {
+      removeItem(i, builder, duration: duration);
+    }
+  }
 }
diff --git a/packages/flutter/test/widgets/animated_grid_test.dart b/packages/flutter/test/widgets/animated_grid_test.dart
index 6c3746b..7a86f65 100644
--- a/packages/flutter/test/widgets/animated_grid_test.dart
+++ b/packages/flutter/test/widgets/animated_grid_test.dart
@@ -103,6 +103,32 @@
 
     await tester.pumpAndSettle();
     expect(find.text('removing item'), findsNothing);
+
+    listKey.currentState!.insertAllItems(0, 2);
+    await tester.pump();
+    expect(find.text('item 2'), findsOneWidget);
+    expect(find.text('item 3'), findsOneWidget);
+
+    // Test for removeAllItems.
+    listKey.currentState!.removeAllItems(
+          (BuildContext context, Animation<double> animation) {
+        return const SizedBox(
+          height: 100.0,
+          child: Center(child: Text('removing item')),
+        );
+      },
+      duration: const Duration(milliseconds: 100),
+    );
+
+    await tester.pump();
+    expect(find.text('removing item'), findsWidgets);
+    expect(find.text('item 0'), findsNothing);
+    expect(find.text('item 1'), findsNothing);
+    expect(find.text('item 2'), findsNothing);
+    expect(find.text('item 3'), findsNothing);
+
+    await tester.pumpAndSettle();
+    expect(find.text('removing item'), findsNothing);
   });
 
   group('SliverAnimatedGrid', () {
@@ -224,6 +250,62 @@
       expect(itemRight(2), 300.0);
     });
 
+    testWidgets('insertAll', (WidgetTester tester) async {
+      final GlobalKey<SliverAnimatedGridState> listKey = GlobalKey<SliverAnimatedGridState>();
+
+      await tester.pumpWidget(
+        Directionality(
+          textDirection: TextDirection.ltr,
+          child: CustomScrollView(
+            slivers: <Widget>[
+              SliverAnimatedGrid(
+                key: listKey,
+                itemBuilder: (BuildContext context, int index, Animation<double> animation) {
+                  return ScaleTransition(
+                    key: ValueKey<int>(index),
+                    scale: animation,
+                    child: SizedBox(
+                      height: 100.0,
+                      child: Center(child: Text('item $index')),
+                    ),
+                  );
+                },
+                gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent(
+                  maxCrossAxisExtent: 100.0,
+                ),
+              ),
+            ],
+          ),
+        ),
+      );
+
+      double itemScale(int index) =>
+          tester.widget<ScaleTransition>(find.byKey(ValueKey<int>(index), skipOffstage: false)).scale.value;
+      double itemLeft(int index) => tester.getTopLeft(find.byKey(ValueKey<int>(index), skipOffstage: false)).dx;
+      double itemRight(int index) => tester.getTopRight(find.byKey(ValueKey<int>(index), skipOffstage: false)).dx;
+
+      listKey.currentState!.insertAllItems(0, 2, duration: const Duration(milliseconds: 100));
+      await tester.pump();
+
+      // Newly inserted items 0 & 1's scale should animate from 0 to 1
+      expect(itemScale(0), 0.0);
+      expect(itemScale(1), 0.0);
+      await tester.pump(const Duration(milliseconds: 50));
+      expect(itemScale(0), 0.5);
+      expect(itemScale(1), 0.5);
+      await tester.pump(const Duration(milliseconds: 50));
+      expect(itemScale(0), 1.0);
+      expect(itemScale(1), 1.0);
+
+      // The list now contains two fully expanded items at the top:
+      expect(find.text('item 0'), findsOneWidget);
+      expect(find.text('item 1'), findsOneWidget);
+      expect(itemLeft(0), 0.0);
+      expect(itemRight(0), 100.0);
+      expect(itemLeft(1), 100.0);
+      expect(itemRight(1), 200.0);
+    });
+
     testWidgets('remove', (WidgetTester tester) async {
       final GlobalKey<SliverAnimatedGridState> listKey = GlobalKey<SliverAnimatedGridState>();
       final List<int> items = <int>[0, 1, 2];
@@ -302,6 +384,58 @@
       expect(itemRight(2), 200.0);
     });
 
+    testWidgets('removeAll', (WidgetTester tester) async {
+      final GlobalKey<SliverAnimatedGridState> listKey = GlobalKey<SliverAnimatedGridState>();
+      final List<int> items = <int>[0, 1, 2];
+
+      Widget buildItem(BuildContext context, int item, Animation<double> animation) {
+        return ScaleTransition(
+          key: ValueKey<int>(item),
+          scale: animation,
+          child: SizedBox(
+            height: 100.0,
+            child: Center(
+              child: Text('item $item', textDirection: TextDirection.ltr),
+            ),
+          ),
+        );
+      }
+
+      await tester.pumpWidget(
+        Directionality(
+          textDirection: TextDirection.ltr,
+          child: CustomScrollView(
+            slivers: <Widget>[
+              SliverAnimatedGrid(
+                key: listKey,
+                initialItemCount: 3,
+                itemBuilder: (BuildContext context, int index, Animation<double> animation) {
+                  return buildItem(context, items[index], animation);
+                },
+                gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent(
+                  maxCrossAxisExtent: 100.0,
+                ),
+              ),
+            ],
+          ),
+        ),
+      );
+      expect(find.text('item 0'), findsOneWidget);
+      expect(find.text('item 1'), findsOneWidget);
+      expect(find.text('item 2'), findsOneWidget);
+
+      items.clear();
+      listKey.currentState!.removeAllItems((BuildContext context, Animation<double> animation) => buildItem(context, 0, animation),
+        duration: const Duration(milliseconds: 100),
+      );
+
+      await tester.pumpAndSettle();
+
+      expect(find.text('item 0'), findsNothing);
+      expect(find.text('item 1'), findsNothing);
+      expect(find.text('item 2'), findsNothing);
+    });
+
     testWidgets('works in combination with other slivers', (WidgetTester tester) async {
       final GlobalKey<SliverAnimatedGridState> listKey = GlobalKey<SliverAnimatedGridState>();
 
diff --git a/packages/flutter/test/widgets/animated_list_test.dart b/packages/flutter/test/widgets/animated_list_test.dart
index 03f4d63..89a6ce6 100644
--- a/packages/flutter/test/widgets/animated_list_test.dart
+++ b/packages/flutter/test/widgets/animated_list_test.dart
@@ -96,6 +96,33 @@
 
     await tester.pumpAndSettle();
     expect(find.text('removing item'), findsNothing);
+
+    // Test for insertAllItems
+    listKey.currentState!.insertAllItems(0, 2);
+    await tester.pump();
+    expect(find.text('item 2'), findsOneWidget);
+    expect(find.text('item 3'), findsOneWidget);
+
+    // Test for removeAllItems
+    listKey.currentState!.removeAllItems(
+      (BuildContext context, Animation<double> animation) {
+        return const SizedBox(
+          height: 100.0,
+          child: Center(child: Text('removing item')),
+        );
+      },
+      duration: const Duration(milliseconds: 100),
+    );
+
+    await tester.pump();
+    expect(find.text('removing item'), findsWidgets);
+    expect(find.text('item 0'), findsNothing);
+    expect(find.text('item 1'), findsNothing);
+    expect(find.text('item 2'), findsNothing);
+    expect(find.text('item 3'), findsNothing);
+
+    await tester.pumpAndSettle();
+    expect(find.text('removing item'), findsNothing);
   });
 
   group('SliverAnimatedList', () {
@@ -217,6 +244,64 @@
       expect(itemBottom(2), 300.0);
     });
 
+    // Test for insertAllItems with SliverAnimatedList
+    testWidgets('insertAll', (WidgetTester tester) async {
+      final GlobalKey<SliverAnimatedListState> listKey = GlobalKey<SliverAnimatedListState>();
+
+      await tester.pumpWidget(
+        Directionality(
+          textDirection: TextDirection.ltr,
+          child: CustomScrollView(
+            slivers: <Widget>[
+              SliverAnimatedList(
+                key: listKey,
+                itemBuilder: (BuildContext context, int index, Animation<double> animation) {
+                  return SizeTransition(
+                    key: ValueKey<int>(index),
+                    sizeFactor: animation,
+                    child: SizedBox(
+                      height: 100.0,
+                      child: Center(child: Text('item $index')),
+                    ),
+                  );
+                },
+              ),
+            ],
+          ),
+        ),
+      );
+
+      double itemHeight(int index) => tester.getSize(find.byKey(ValueKey<int>(index), skipOffstage: false)).height;
+      double itemTop(int index) => tester.getTopLeft(find.byKey(ValueKey<int>(index), skipOffstage: false)).dy;
+      double itemBottom(int index) => tester.getBottomLeft(find.byKey(ValueKey<int>(index), skipOffstage: false)).dy;
+
+      listKey.currentState!.insertAllItems(
+        0,
+        2,
+        duration: const Duration(milliseconds: 100),
+      );
+      await tester.pump();
+
+      // Newly inserted item 0 & 1's height should animate from 0 to 100
+      expect(itemHeight(0), 0.0);
+      expect(itemHeight(1), 0.0);
+      await tester.pump(const Duration(milliseconds: 50));
+      expect(itemHeight(0), 50.0);
+      expect(itemHeight(1), 50.0);
+      await tester.pump(const Duration(milliseconds: 50));
+      expect(itemHeight(0), 100.0);
+      expect(itemHeight(1), 100.0);
+
+      // The list now contains two fully expanded items at the top:
+      expect(find.text('item 0'), findsOneWidget);
+      expect(find.text('item 1'), findsOneWidget);
+      expect(itemTop(0), 0.0);
+      expect(itemBottom(0), 100.0);
+      expect(itemTop(1), 100.0);
+      expect(itemBottom(1), 200.0);
+    });
+
+    // Test for removeAllItems with SliverAnimatedList
     testWidgets('remove', (WidgetTester tester) async {
       final GlobalKey<SliverAnimatedListState> listKey = GlobalKey<SliverAnimatedListState>();
       final List<int> items = <int>[0, 1, 2];
@@ -293,6 +378,57 @@
       expect(itemBottom(2), 200.0);
     });
 
+    // Test for removeAllItems with SliverAnimatedList
+    testWidgets('removeAll', (WidgetTester tester) async {
+      final GlobalKey<SliverAnimatedListState> listKey = GlobalKey<SliverAnimatedListState>();
+      final List<int> items = <int>[0, 1, 2];
+
+      Widget buildItem(BuildContext context, int item, Animation<double> animation) {
+        return SizeTransition(
+          key: ValueKey<int>(item),
+          sizeFactor: animation,
+          child: SizedBox(
+            height: 100.0,
+            child: Center(
+              child: Text('item $item', textDirection: TextDirection.ltr),
+            ),
+          ),
+        );
+      }
+
+      await tester.pumpWidget(
+        Directionality(
+          textDirection: TextDirection.ltr,
+          child: CustomScrollView(
+            slivers: <Widget>[
+              SliverAnimatedList(
+                key: listKey,
+                initialItemCount: 3,
+                itemBuilder: (BuildContext context, int index, Animation<double> animation) {
+                  return buildItem(context, items[index], animation);
+                },
+              ),
+            ],
+          ),
+        ),
+      );
+
+      expect(find.text('item 0'), findsOneWidget);
+      expect(find.text('item 1'), findsOneWidget);
+      expect(find.text('item 2'), findsOneWidget);
+
+      items.clear();
+      listKey.currentState!.removeAllItems((BuildContext context, Animation<double> animation) => buildItem(context, 0, animation),
+        duration: const Duration(milliseconds: 100),
+      );
+
+      await tester.pumpAndSettle();
+
+      expect(find.text('item 0'), findsNothing);
+      expect(find.text('item 1'), findsNothing);
+      expect(find.text('item 2'), findsNothing);
+    });
+
     testWidgets('works in combination with other slivers', (WidgetTester tester) async {
       final GlobalKey<SliverAnimatedListState> listKey = GlobalKey<SliverAnimatedListState>();