AnimatedList
and ListView
have similar functionalities, but the key difference is that AnimatedList
can perform animations when inserting or deleting items in the list. This feature enhances the user experience in scenarios where items are added or removed from the list.
AnimatedList
is a StatefulWidget
, and its corresponding state type is AnimatedListState
. The methods for adding and removing elements are found in AnimatedListState
:
void insertItem(int index, { Duration duration = _kDuration }); void removeItem(int index, AnimatedListRemovedItemBuilder builder, { Duration duration = _kDuration });
Let’s look at an example: we will create a list that allows users to add a new item by clicking a "+" button at the bottom, and delete an item by clicking the delete button next to each list item. The specified animations will be executed when items are added or removed, as shown in Figure.
Initially, there are five items in the list. When the "+" button is clicked, a "6" is added, and a fade-in animation occurs during the addition. After clicking the delete button next to "4," a fade-out and shrink animation is executed.
Here’s the implementation code:
class AnimatedListRoute extends StatefulWidget { const AnimatedListRoute({Key? key}) : super(key: key); @override _AnimatedListRouteState createState() => _AnimatedListRouteState(); } class _AnimatedListRouteState extends State<AnimatedListRoute> { var data = <String>[]; int counter = 5; final globalKey = GlobalKey<AnimatedListState>(); @override void initState() { for (var i = 0; i < counter; i++) { data.add('${i + 1}'); } super.initState(); } @override Widget build(BuildContext context) { return Stack( children: [ AnimatedList( key: globalKey, initialItemCount: data.length, itemBuilder: ( BuildContext context, int index, Animation<double> animation, ) { // Fade-in animation when adding list items return FadeTransition( opacity: animation, child: buildItem(context, index), ); }, ), buildAddBtn(), ], ); } // Create a "+" button to insert an item into the list Widget buildAddBtn() { return Positioned( child: FloatingActionButton( child: Icon(Icons.add), onPressed: () { // Add a new list item data.add('${++counter}'); // Notify the list that a new item has been added globalKey.currentState!.insertItem(data.length - 1); print('Added $counter'); }, ), bottom: 30, left: 0, right: 0, ); } // Build a list item Widget buildItem(context, index) { String char = data[index]; return ListTile( // The number won't repeat, so it's used as the key key: ValueKey(char), title: Text(char), trailing: IconButton( icon: Icon(Icons.delete), // Delete on click onPressed: () => onDelete(context, index), ), ); } void onDelete(context, index) { // To be implemented } }
When deleting, we need to use the removeItem
method from AnimatedListState
to apply the deletion animation, with the specific logic in the onDelete
method:
setState(() { globalKey.currentState!.removeItem( index, (context, animation) { // The deletion process executes a reverse animation; animation.value changes from 1 to 0 var item = buildItem(context, index); print('Deleted ${data[index]}'); data.removeAt(index); // The deletion animation is a composite animation: fade-out + shrink return FadeTransition( opacity: CurvedAnimation( parent: animation, // Make the opacity change faster curve: const Interval(0.5, 1.0), ), // Continuously reduce the height of the list item child: SizeTransition( sizeFactor: animation, axisAlignment: 0.0, child: item, ), ); }, duration: Duration(milliseconds: 200), // Animation duration is 200 ms ); });
The code is simple, but we need to note that our data is maintained separately in data
. Calling the insertion and removal methods of AnimatedListState
is essentially a notification: indicating where to execute the insertion or removal animation. The data remains driven (reactive rather than imperative).