Skip to content
Blog

AnimatedList in Flutter: Smooth Insert and Remove Animations

Animate rows entering and leaving a Flutter list with AnimatedList: GlobalKey setup, index-synced insert/remove, a SizeTransition exit builder, and test tips.

Published on • November 2, 2026

AI Assistant

A ListView that snaps rows in and out with no feedback feels abrupt the moment users add or delete items. AnimatedList drives an Animation<double> per changing row so its neighbors slide into place instead (docs.flutter.dev).

Setting Up AnimatedList

Reach the list through a GlobalKey<AnimatedListState>, and pass an initialItemCount that matches your starting data. The itemBuilder(index, animation) hands you that row’s animation — it runs 0→1 on insert and holds at 1 for rows already on screen:

final _listKey = GlobalKey<AnimatedListState>();
final List<String> _items = ['Alpha', 'Bravo', 'Charlie'];

AnimatedList(
  key: _listKey,
  initialItemCount: _items.length,
  itemBuilder: (context, index, animation) => SizeTransition(
    sizeFactor: animation,
    child: ListTile(title: Text(_items[index])),
  ),
)

Keep the Backing List in Sync

AnimatedList never mutates your data — you update _items and the state yourself, in the same order, using the same index:

void _insert(int index, String value) {
  _items.insert(index, value); // data first …
  _listKey.currentState!.insertItem(index,
      duration: const Duration(milliseconds: 350));
}

void _removeAt(int index) {
  final removed = _items.removeAt(index); // … then the animation
  _listKey.currentState!.removeItem(index,
      (context, animation) => _buildRow(removed, animation),
      duration: const Duration(milliseconds: 300));
}

Mutating after the state call makes itemBuilder read a stale index and throw a RangeError; reversing the order renders the wrong row.

Animating the Outgoing Row

removeItem takes a second builder for the leaving row — it runs while the rest of the list closes the gap. Wrapping it in a SizeTransition collapses the row as it fades:

Widget _buildRow(String item, Animation<double> animation) => SizeTransition(
      sizeFactor: animation,
      child: FadeTransition(
        opacity: animation,
        child: ListTile(title: Text(item), trailing: const Icon(Icons.delete_outline)),
      ),
    );

Test Tips for AnimatedList

Animations keep the test clock busy, so settle them with tester.pumpAndSettle() before asserting; for an infinite animation (say, a spinner), pump a fixed duration instead of settling:

await tester.tap(find.byIcon(Icons.add));
await tester.pumpAndSettle();
expect(find.text('Delta'), findsOneWidget);

When SliverList + AnimatedSwitcher Is Enough

If rows change content rather than count, AnimatedList is overkill: a plain ListView whose children wrap in AnimatedSwitcher animates the swap with zero index bookkeeping. Reserve AnimatedList for real inserts and removals.

Sync the data with the animation and AnimatedList turns jagged list edits into predictable motion.