← Back to Blog

Flutter AnimatedList Localization: Dynamic Lists, Insertion Animations, and Localized Feedback

flutteranimatedlistanimationlistslocalizationaccessibility

Flutter AnimatedList Localization: Dynamic Lists, Insertion Animations, and Localized Feedback

AnimatedList provides smooth animations when items are added or removed from a list. Proper localization ensures that insertion messages, removal feedback, empty states, and accessibility announcements work seamlessly across all languages. This guide covers everything you need to know about localizing AnimatedList widgets in Flutter.

Understanding AnimatedList Localization

AnimatedList widgets require localization for:

  • Empty state messages: What to show when the list is empty
  • Insertion feedback: Messages when items are added
  • Removal feedback: Messages when items are removed
  • Item content: The actual content of each list item
  • Action buttons: Add, remove, and edit button labels
  • Accessibility announcements: Screen reader feedback for list changes

Basic AnimatedList with Localized Content

Start with a simple animated list with localized messages:

import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';

class LocalizedAnimatedList extends StatefulWidget {
  const LocalizedAnimatedList({super.key});

  @override
  State<LocalizedAnimatedList> createState() => _LocalizedAnimatedListState();
}

class _LocalizedAnimatedListState extends State<LocalizedAnimatedList> {
  final GlobalKey<AnimatedListState> _listKey = GlobalKey<AnimatedListState>();
  final List<String> _items = [];
  int _counter = 0;

  @override
  Widget build(BuildContext context) {
    final l10n = AppLocalizations.of(context)!;

    return Scaffold(
      appBar: AppBar(
        title: Text(l10n.shoppingListTitle),
        actions: [
          IconButton(
            icon: const Icon(Icons.delete_sweep),
            tooltip: l10n.clearAllTooltip,
            onPressed: _items.isEmpty ? null : _clearAll,
          ),
        ],
      ),
      body: _items.isEmpty
          ? _buildEmptyState(context, l10n)
          : AnimatedList(
              key: _listKey,
              initialItemCount: _items.length,
              padding: const EdgeInsets.all(16),
              itemBuilder: (context, index, animation) {
                return _buildItem(context, _items[index], index, animation);
              },
            ),
      floatingActionButton: FloatingActionButton.extended(
        onPressed: _addItem,
        icon: const Icon(Icons.add),
        label: Text(l10n.addItemButton),
      ),
    );
  }

  Widget _buildEmptyState(BuildContext context, AppLocalizations l10n) {
    return Center(
      child: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
          Icon(
            Icons.shopping_cart_outlined,
            size: 80,
            color: Colors.grey[400],
          ),
          const SizedBox(height: 16),
          Text(
            l10n.emptyListTitle,
            style: Theme.of(context).textTheme.headlineSmall,
          ),
          const SizedBox(height: 8),
          Text(
            l10n.emptyListSubtitle,
            style: TextStyle(color: Colors.grey[600]),
          ),
        ],
      ),
    );
  }

  Widget _buildItem(
    BuildContext context,
    String item,
    int index,
    Animation<double> animation,
  ) {
    final l10n = AppLocalizations.of(context)!;

    return SizeTransition(
      sizeFactor: animation,
      child: FadeTransition(
        opacity: animation,
        child: Card(
          margin: const EdgeInsets.only(bottom: 8),
          child: ListTile(
            leading: CircleAvatar(
              child: Text('${index + 1}'),
            ),
            title: Text(item),
            trailing: IconButton(
              icon: const Icon(Icons.delete),
              tooltip: l10n.removeItemTooltip,
              onPressed: () => _removeItem(index),
            ),
          ),
        ),
      ),
    );
  }

  void _addItem() {
    final l10n = AppLocalizations.of(context)!;
    _counter++;
    final newItem = l10n.itemName(_counter);
    final index = _items.length;

    _items.add(newItem);
    _listKey.currentState?.insertItem(
      index,
      duration: const Duration(milliseconds: 300),
    );

    ScaffoldMessenger.of(context).showSnackBar(
      SnackBar(
        content: Text(l10n.itemAdded(newItem)),
        duration: const Duration(seconds: 2),
      ),
    );
  }

  void _removeItem(int index) {
    final l10n = AppLocalizations.of(context)!;
    final removedItem = _items[index];

    _items.removeAt(index);
    _listKey.currentState?.removeItem(
      index,
      (context, animation) => _buildRemovedItem(removedItem, animation),
      duration: const Duration(milliseconds: 300),
    );

    ScaffoldMessenger.of(context).showSnackBar(
      SnackBar(
        content: Text(l10n.itemRemoved(removedItem)),
        action: SnackBarAction(
          label: l10n.undoAction,
          onPressed: () {
            _items.insert(index, removedItem);
            _listKey.currentState?.insertItem(index);
          },
        ),
      ),
    );
  }

  Widget _buildRemovedItem(String item, Animation<double> animation) {
    return SizeTransition(
      sizeFactor: animation,
      child: FadeTransition(
        opacity: animation,
        child: Card(
          margin: const EdgeInsets.only(bottom: 8),
          color: Colors.red[100],
          child: ListTile(
            title: Text(
              item,
              style: const TextStyle(
                decoration: TextDecoration.lineThrough,
              ),
            ),
          ),
        ),
      ),
    );
  }

  void _clearAll() {
    final l10n = AppLocalizations.of(context)!;
    final itemsCopy = List<String>.from(_items);

    for (var i = _items.length - 1; i >= 0; i--) {
      _listKey.currentState?.removeItem(
        i,
        (context, animation) => _buildRemovedItem(_items[i], animation),
        duration: Duration(milliseconds: 100 + (i * 50)),
      );
    }
    _items.clear();

    ScaffoldMessenger.of(context).showSnackBar(
      SnackBar(
        content: Text(l10n.allItemsCleared(itemsCopy.length)),
        action: SnackBarAction(
          label: l10n.undoAction,
          onPressed: () {
            for (var i = 0; i < itemsCopy.length; i++) {
              _items.add(itemsCopy[i]);
              _listKey.currentState?.insertItem(i);
            }
          },
        ),
      ),
    );
  }
}

AnimatedList with Categories

Organize items with localized section headers:

class CategorizedAnimatedList extends StatefulWidget {
  const CategorizedAnimatedList({super.key});

  @override
  State<CategorizedAnimatedList> createState() => _CategorizedAnimatedListState();
}

class _CategorizedAnimatedListState extends State<CategorizedAnimatedList> {
  final GlobalKey<AnimatedListState> _listKey = GlobalKey<AnimatedListState>();
  late List<GroceryItem> _items;

  @override
  void didChangeDependencies() {
    super.didChangeDependencies();
    _initializeItems();
  }

  void _initializeItems() {
    final l10n = AppLocalizations.of(context)!;
    _items = [
      GroceryItem(
        id: '1',
        name: l10n.itemMilk,
        category: l10n.categoryDairy,
        quantity: 2,
      ),
      GroceryItem(
        id: '2',
        name: l10n.itemBread,
        category: l10n.categoryBakery,
        quantity: 1,
      ),
      GroceryItem(
        id: '3',
        name: l10n.itemApples,
        category: l10n.categoryProduce,
        quantity: 6,
      ),
    ];
  }

  @override
  Widget build(BuildContext context) {
    final l10n = AppLocalizations.of(context)!;

    return Scaffold(
      appBar: AppBar(
        title: Text(l10n.groceryListTitle),
        actions: [
          PopupMenuButton<String>(
            tooltip: l10n.addByCategoryTooltip,
            icon: const Icon(Icons.add),
            onSelected: (category) => _showAddItemDialog(category),
            itemBuilder: (context) => [
              PopupMenuItem(
                value: l10n.categoryDairy,
                child: Row(
                  children: [
                    const Icon(Icons.egg_alt),
                    const SizedBox(width: 8),
                    Text(l10n.categoryDairy),
                  ],
                ),
              ),
              PopupMenuItem(
                value: l10n.categoryBakery,
                child: Row(
                  children: [
                    const Icon(Icons.bakery_dining),
                    const SizedBox(width: 8),
                    Text(l10n.categoryBakery),
                  ],
                ),
              ),
              PopupMenuItem(
                value: l10n.categoryProduce,
                child: Row(
                  children: [
                    const Icon(Icons.eco),
                    const SizedBox(width: 8),
                    Text(l10n.categoryProduce),
                  ],
                ),
              ),
            ],
          ),
        ],
      ),
      body: _items.isEmpty
          ? _buildEmptyState(context, l10n)
          : AnimatedList(
              key: _listKey,
              initialItemCount: _items.length,
              padding: const EdgeInsets.all(16),
              itemBuilder: (context, index, animation) {
                return _buildAnimatedItem(_items[index], index, animation);
              },
            ),
    );
  }

  Widget _buildEmptyState(BuildContext context, AppLocalizations l10n) {
    return Center(
      child: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
          const Icon(Icons.shopping_basket, size: 80, color: Colors.grey),
          const SizedBox(height: 16),
          Text(l10n.emptyGroceryList),
          const SizedBox(height: 8),
          Text(l10n.tapToAddItems),
        ],
      ),
    );
  }

  Widget _buildAnimatedItem(
    GroceryItem item,
    int index,
    Animation<double> animation,
  ) {
    final l10n = AppLocalizations.of(context)!;

    return SlideTransition(
      position: animation.drive(
        Tween(
          begin: const Offset(1, 0),
          end: Offset.zero,
        ).chain(CurveTween(curve: Curves.easeOutCubic)),
      ),
      child: FadeTransition(
        opacity: animation,
        child: Card(
          margin: const EdgeInsets.only(bottom: 8),
          child: ListTile(
            leading: _getCategoryIcon(item.category),
            title: Text(item.name),
            subtitle: Text(
              l10n.itemQuantity(item.quantity, item.category),
            ),
            trailing: Row(
              mainAxisSize: MainAxisSize.min,
              children: [
                IconButton(
                  icon: const Icon(Icons.remove),
                  tooltip: l10n.decreaseQuantity,
                  onPressed: item.quantity > 1
                      ? () => _updateQuantity(index, -1)
                      : null,
                ),
                Text(
                  '${item.quantity}',
                  style: const TextStyle(fontSize: 16),
                ),
                IconButton(
                  icon: const Icon(Icons.add),
                  tooltip: l10n.increaseQuantity,
                  onPressed: () => _updateQuantity(index, 1),
                ),
                IconButton(
                  icon: const Icon(Icons.delete),
                  tooltip: l10n.removeItemTooltip,
                  onPressed: () => _removeItem(index),
                ),
              ],
            ),
          ),
        ),
      ),
    );
  }

  Widget _getCategoryIcon(String category) {
    final l10n = AppLocalizations.of(context)!;

    if (category == l10n.categoryDairy) {
      return const CircleAvatar(
        backgroundColor: Colors.blue,
        child: Icon(Icons.egg_alt, color: Colors.white),
      );
    } else if (category == l10n.categoryBakery) {
      return const CircleAvatar(
        backgroundColor: Colors.orange,
        child: Icon(Icons.bakery_dining, color: Colors.white),
      );
    } else {
      return const CircleAvatar(
        backgroundColor: Colors.green,
        child: Icon(Icons.eco, color: Colors.white),
      );
    }
  }

  void _updateQuantity(int index, int delta) {
    setState(() {
      _items[index] = _items[index].copyWith(
        quantity: _items[index].quantity + delta,
      );
    });
  }

  void _removeItem(int index) {
    final l10n = AppLocalizations.of(context)!;
    final item = _items[index];

    setState(() {
      _items.removeAt(index);
    });

    _listKey.currentState?.removeItem(
      index,
      (context, animation) => _buildRemovedItem(item, animation),
      duration: const Duration(milliseconds: 300),
    );

    ScaffoldMessenger.of(context).showSnackBar(
      SnackBar(
        content: Text(l10n.itemRemoved(item.name)),
        action: SnackBarAction(
          label: l10n.undoAction,
          onPressed: () {
            setState(() {
              _items.insert(index, item);
            });
            _listKey.currentState?.insertItem(index);
          },
        ),
      ),
    );
  }

  Widget _buildRemovedItem(GroceryItem item, Animation<double> animation) {
    return SizeTransition(
      sizeFactor: animation,
      child: Card(
        color: Colors.grey[300],
        margin: const EdgeInsets.only(bottom: 8),
        child: ListTile(
          title: Text(
            item.name,
            style: const TextStyle(decoration: TextDecoration.lineThrough),
          ),
        ),
      ),
    );
  }

  void _showAddItemDialog(String category) {
    final l10n = AppLocalizations.of(context)!;
    final controller = TextEditingController();

    showDialog(
      context: context,
      builder: (context) => AlertDialog(
        title: Text(l10n.addItemToCategory(category)),
        content: TextField(
          controller: controller,
          autofocus: true,
          decoration: InputDecoration(
            labelText: l10n.itemNameLabel,
            hintText: l10n.itemNameHint,
          ),
        ),
        actions: [
          TextButton(
            onPressed: () => Navigator.pop(context),
            child: Text(l10n.cancelAction),
          ),
          TextButton(
            onPressed: () {
              if (controller.text.isNotEmpty) {
                Navigator.pop(context);
                _addItem(controller.text, category);
              }
            },
            child: Text(l10n.addAction),
          ),
        ],
      ),
    );
  }

  void _addItem(String name, String category) {
    final l10n = AppLocalizations.of(context)!;
    final newItem = GroceryItem(
      id: DateTime.now().millisecondsSinceEpoch.toString(),
      name: name,
      category: category,
      quantity: 1,
    );

    final index = _items.length;
    setState(() {
      _items.add(newItem);
    });
    _listKey.currentState?.insertItem(index);

    ScaffoldMessenger.of(context).showSnackBar(
      SnackBar(content: Text(l10n.itemAddedToCategory(name, category))),
    );
  }
}

class GroceryItem {
  final String id;
  final String name;
  final String category;
  final int quantity;

  GroceryItem({
    required this.id,
    required this.name,
    required this.category,
    required this.quantity,
  });

  GroceryItem copyWith({
    String? id,
    String? name,
    String? category,
    int? quantity,
  }) {
    return GroceryItem(
      id: id ?? this.id,
      name: name ?? this.name,
      category: category ?? this.category,
      quantity: quantity ?? this.quantity,
    );
  }
}

AnimatedList with Reordering

Support drag-to-reorder with localized feedback:

class ReorderableAnimatedList extends StatefulWidget {
  const ReorderableAnimatedList({super.key});

  @override
  State<ReorderableAnimatedList> createState() => _ReorderableAnimatedListState();
}

class _ReorderableAnimatedListState extends State<ReorderableAnimatedList> {
  final GlobalKey<AnimatedListState> _listKey = GlobalKey<AnimatedListState>();
  late List<PriorityTask> _tasks;

  @override
  void didChangeDependencies() {
    super.didChangeDependencies();
    _initializeTasks();
  }

  void _initializeTasks() {
    final l10n = AppLocalizations.of(context)!;
    _tasks = [
      PriorityTask(id: '1', title: l10n.taskReviewCode, priority: 1),
      PriorityTask(id: '2', title: l10n.taskWriteTests, priority: 2),
      PriorityTask(id: '3', title: l10n.taskUpdateDocs, priority: 3),
      PriorityTask(id: '4', title: l10n.taskDeployApp, priority: 4),
    ];
  }

  @override
  Widget build(BuildContext context) {
    final l10n = AppLocalizations.of(context)!;

    return Scaffold(
      appBar: AppBar(
        title: Text(l10n.priorityTasksTitle),
        actions: [
          IconButton(
            icon: const Icon(Icons.sort),
            tooltip: l10n.sortByPriorityTooltip,
            onPressed: _sortByPriority,
          ),
        ],
      ),
      body: _tasks.isEmpty
          ? Center(child: Text(l10n.noTasksMessage))
          : ReorderableListView.builder(
              padding: const EdgeInsets.all(16),
              itemCount: _tasks.length,
              onReorder: _reorderTasks,
              itemBuilder: (context, index) {
                final task = _tasks[index];
                return _buildTaskItem(task, index);
              },
            ),
      floatingActionButton: FloatingActionButton(
        onPressed: _addTask,
        tooltip: l10n.addTaskTooltip,
        child: const Icon(Icons.add),
      ),
    );
  }

  Widget _buildTaskItem(PriorityTask task, int index) {
    final l10n = AppLocalizations.of(context)!;

    return Card(
      key: Key(task.id),
      margin: const EdgeInsets.only(bottom: 8),
      child: ListTile(
        leading: CircleAvatar(
          backgroundColor: _getPriorityColor(task.priority),
          child: Text(
            '${task.priority}',
            style: const TextStyle(color: Colors.white),
          ),
        ),
        title: Text(task.title),
        subtitle: Text(l10n.priorityLevel(task.priority)),
        trailing: Row(
          mainAxisSize: MainAxisSize.min,
          children: [
            IconButton(
              icon: const Icon(Icons.delete),
              tooltip: l10n.deleteTaskTooltip,
              onPressed: () => _removeTask(index),
            ),
            ReorderableDragStartListener(
              index: index,
              child: const Icon(Icons.drag_handle),
            ),
          ],
        ),
      ),
    );
  }

  Color _getPriorityColor(int priority) {
    if (priority <= 2) return Colors.red;
    if (priority <= 4) return Colors.orange;
    return Colors.green;
  }

  void _reorderTasks(int oldIndex, int newIndex) {
    final l10n = AppLocalizations.of(context)!;

    setState(() {
      if (newIndex > oldIndex) newIndex--;
      final task = _tasks.removeAt(oldIndex);
      _tasks.insert(newIndex, task);

      // Update priorities
      for (var i = 0; i < _tasks.length; i++) {
        _tasks[i] = _tasks[i].copyWith(priority: i + 1);
      }
    });

    ScaffoldMessenger.of(context).showSnackBar(
      SnackBar(
        content: Text(l10n.taskReordered(_tasks[newIndex].title, newIndex + 1)),
        duration: const Duration(seconds: 2),
      ),
    );
  }

  void _removeTask(int index) {
    final l10n = AppLocalizations.of(context)!;
    final task = _tasks[index];

    setState(() {
      _tasks.removeAt(index);
      // Update priorities
      for (var i = 0; i < _tasks.length; i++) {
        _tasks[i] = _tasks[i].copyWith(priority: i + 1);
      }
    });

    ScaffoldMessenger.of(context).showSnackBar(
      SnackBar(
        content: Text(l10n.taskRemoved(task.title)),
        action: SnackBarAction(
          label: l10n.undoAction,
          onPressed: () {
            setState(() {
              _tasks.insert(index, task);
              for (var i = 0; i < _tasks.length; i++) {
                _tasks[i] = _tasks[i].copyWith(priority: i + 1);
              }
            });
          },
        ),
      ),
    );
  }

  void _sortByPriority() {
    final l10n = AppLocalizations.of(context)!;

    setState(() {
      _tasks.sort((a, b) => a.priority.compareTo(b.priority));
    });

    ScaffoldMessenger.of(context).showSnackBar(
      SnackBar(content: Text(l10n.tasksSortedByPriority)),
    );
  }

  void _addTask() {
    final l10n = AppLocalizations.of(context)!;
    final controller = TextEditingController();

    showDialog(
      context: context,
      builder: (context) => AlertDialog(
        title: Text(l10n.addNewTask),
        content: TextField(
          controller: controller,
          autofocus: true,
          decoration: InputDecoration(
            labelText: l10n.taskTitleLabel,
            hintText: l10n.taskTitleHint,
          ),
        ),
        actions: [
          TextButton(
            onPressed: () => Navigator.pop(context),
            child: Text(l10n.cancelAction),
          ),
          TextButton(
            onPressed: () {
              if (controller.text.isNotEmpty) {
                Navigator.pop(context);
                setState(() {
                  _tasks.add(PriorityTask(
                    id: DateTime.now().millisecondsSinceEpoch.toString(),
                    title: controller.text,
                    priority: _tasks.length + 1,
                  ));
                });
                ScaffoldMessenger.of(context).showSnackBar(
                  SnackBar(content: Text(l10n.taskAdded(controller.text))),
                );
              }
            },
            child: Text(l10n.addAction),
          ),
        ],
      ),
    );
  }
}

class PriorityTask {
  final String id;
  final String title;
  final int priority;

  PriorityTask({
    required this.id,
    required this.title,
    required this.priority,
  });

  PriorityTask copyWith({String? id, String? title, int? priority}) {
    return PriorityTask(
      id: id ?? this.id,
      title: title ?? this.title,
      priority: priority ?? this.priority,
    );
  }
}

Accessible AnimatedList

Provide full accessibility support:

class AccessibleAnimatedList extends StatefulWidget {
  const AccessibleAnimatedList({super.key});

  @override
  State<AccessibleAnimatedList> createState() => _AccessibleAnimatedListState();
}

class _AccessibleAnimatedListState extends State<AccessibleAnimatedList> {
  final GlobalKey<AnimatedListState> _listKey = GlobalKey<AnimatedListState>();
  final List<NotificationItem> _notifications = [];

  @override
  Widget build(BuildContext context) {
    final l10n = AppLocalizations.of(context)!;

    return Scaffold(
      appBar: AppBar(
        title: Semantics(
          header: true,
          child: Text(l10n.notificationsTitle),
        ),
      ),
      body: Column(
        children: [
          // Summary for screen readers
          Semantics(
            liveRegion: true,
            child: Padding(
              padding: const EdgeInsets.all(16),
              child: Text(
                l10n.notificationCount(_notifications.length),
                style: Theme.of(context).textTheme.bodyLarge,
              ),
            ),
          ),
          Expanded(
            child: _notifications.isEmpty
                ? Semantics(
                    label: l10n.noNotificationsAccessibilityLabel,
                    child: Center(
                      child: Column(
                        mainAxisAlignment: MainAxisAlignment.center,
                        children: [
                          const Icon(Icons.notifications_none, size: 64),
                          const SizedBox(height: 16),
                          Text(l10n.noNotifications),
                        ],
                      ),
                    ),
                  )
                : AnimatedList(
                    key: _listKey,
                    initialItemCount: _notifications.length,
                    itemBuilder: (context, index, animation) {
                      return _buildAccessibleItem(
                        _notifications[index],
                        index,
                        animation,
                      );
                    },
                  ),
          ),
        ],
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _simulateNewNotification,
        tooltip: l10n.simulateNotificationTooltip,
        child: const Icon(Icons.add_alert),
      ),
    );
  }

  Widget _buildAccessibleItem(
    NotificationItem notification,
    int index,
    Animation<double> animation,
  ) {
    final l10n = AppLocalizations.of(context)!;

    return SizeTransition(
      sizeFactor: animation,
      child: Semantics(
        label: l10n.notificationAccessibilityLabel(
          notification.title,
          notification.timeAgo,
        ),
        hint: l10n.notificationHint,
        customSemanticsActions: {
          CustomSemanticsAction(label: l10n.dismissAction): () {
            _dismissNotification(index);
          },
          CustomSemanticsAction(label: l10n.markAsReadAction): () {
            _markAsRead(index);
          },
        },
        child: Card(
          margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
          color: notification.isRead ? null : Colors.blue[50],
          child: ListTile(
            leading: Icon(
              notification.icon,
              color: notification.isRead ? Colors.grey : Colors.blue,
              semanticLabel: l10n.notificationIconLabel(notification.type),
            ),
            title: Text(
              notification.title,
              style: TextStyle(
                fontWeight: notification.isRead ? null : FontWeight.bold,
              ),
            ),
            subtitle: Text(notification.timeAgo),
            trailing: IconButton(
              icon: const Icon(Icons.close),
              tooltip: l10n.dismissNotification,
              onPressed: () => _dismissNotification(index),
            ),
            onTap: () => _markAsRead(index),
          ),
        ),
      ),
    );
  }

  void _simulateNewNotification() {
    final l10n = AppLocalizations.of(context)!;
    final types = ['message', 'alert', 'update'];
    final type = types[_notifications.length % types.length];

    final notification = NotificationItem(
      id: DateTime.now().millisecondsSinceEpoch.toString(),
      title: l10n.notificationTitle(type),
      type: type,
      timeAgo: l10n.justNow,
      icon: _getIconForType(type),
      isRead: false,
    );

    setState(() {
      _notifications.insert(0, notification);
    });
    _listKey.currentState?.insertItem(0);

    // Announce to screen readers
    SemanticsService.announce(
      l10n.newNotificationAnnouncement(notification.title),
      Directionality.of(context),
    );
  }

  IconData _getIconForType(String type) {
    return switch (type) {
      'message' => Icons.message,
      'alert' => Icons.warning,
      'update' => Icons.system_update,
      _ => Icons.notifications,
    };
  }

  void _dismissNotification(int index) {
    final l10n = AppLocalizations.of(context)!;
    final notification = _notifications[index];

    setState(() {
      _notifications.removeAt(index);
    });

    _listKey.currentState?.removeItem(
      index,
      (context, animation) => SizeTransition(
        sizeFactor: animation,
        child: Card(
          margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
          child: ListTile(title: Text(notification.title)),
        ),
      ),
    );

    // Announce dismissal
    SemanticsService.announce(
      l10n.notificationDismissed(notification.title),
      Directionality.of(context),
    );
  }

  void _markAsRead(int index) {
    final l10n = AppLocalizations.of(context)!;

    if (!_notifications[index].isRead) {
      setState(() {
        _notifications[index] = _notifications[index].copyWith(isRead: true);
      });

      SemanticsService.announce(
        l10n.notificationMarkedAsRead,
        Directionality.of(context),
      );
    }
  }
}

class NotificationItem {
  final String id;
  final String title;
  final String type;
  final String timeAgo;
  final IconData icon;
  final bool isRead;

  NotificationItem({
    required this.id,
    required this.title,
    required this.type,
    required this.timeAgo,
    required this.icon,
    required this.isRead,
  });

  NotificationItem copyWith({
    String? id,
    String? title,
    String? type,
    String? timeAgo,
    IconData? icon,
    bool? isRead,
  }) {
    return NotificationItem(
      id: id ?? this.id,
      title: title ?? this.title,
      type: type ?? this.type,
      timeAgo: timeAgo ?? this.timeAgo,
      icon: icon ?? this.icon,
      isRead: isRead ?? this.isRead,
    );
  }
}

ARB File Structure

Define all AnimatedList-related translations:

{
  "@@locale": "en",

  "shoppingListTitle": "Shopping List",
  "@shoppingListTitle": {
    "description": "Title for shopping list screen"
  },

  "clearAllTooltip": "Clear all items",
  "@clearAllTooltip": {
    "description": "Tooltip for clear all button"
  },

  "addItemButton": "Add Item",
  "@addItemButton": {
    "description": "Add item button label"
  },

  "emptyListTitle": "Your list is empty",
  "@emptyListTitle": {
    "description": "Empty list title"
  },

  "emptyListSubtitle": "Tap the button below to add items",
  "@emptyListSubtitle": {
    "description": "Empty list subtitle"
  },

  "itemName": "Item {number}",
  "@itemName": {
    "description": "Default item name",
    "placeholders": {
      "number": {
        "type": "int"
      }
    }
  },

  "itemAdded": "{item} added to list",
  "@itemAdded": {
    "description": "Message when item is added",
    "placeholders": {
      "item": {
        "type": "String"
      }
    }
  },

  "itemRemoved": "{item} removed",
  "@itemRemoved": {
    "description": "Message when item is removed",
    "placeholders": {
      "item": {
        "type": "String"
      }
    }
  },

  "removeItemTooltip": "Remove item",
  "@removeItemTooltip": {
    "description": "Tooltip for remove button"
  },

  "undoAction": "Undo",
  "@undoAction": {
    "description": "Undo action label"
  },

  "allItemsCleared": "{count, plural, =1{1 item cleared} other{{count} items cleared}}",
  "@allItemsCleared": {
    "description": "Message when all items are cleared",
    "placeholders": {
      "count": {
        "type": "int"
      }
    }
  },

  "groceryListTitle": "Grocery List",
  "@groceryListTitle": {
    "description": "Title for grocery list"
  },

  "categoryDairy": "Dairy",
  "@categoryDairy": {
    "description": "Dairy category"
  },

  "categoryBakery": "Bakery",
  "@categoryBakery": {
    "description": "Bakery category"
  },

  "categoryProduce": "Produce",
  "@categoryProduce": {
    "description": "Produce category"
  },

  "itemMilk": "Milk",
  "@itemMilk": {
    "description": "Milk item"
  },

  "itemBread": "Bread",
  "@itemBread": {
    "description": "Bread item"
  },

  "itemApples": "Apples",
  "@itemApples": {
    "description": "Apples item"
  },

  "addByCategoryTooltip": "Add item by category",
  "@addByCategoryTooltip": {
    "description": "Tooltip for add by category"
  },

  "emptyGroceryList": "Your grocery list is empty",
  "@emptyGroceryList": {
    "description": "Empty grocery list message"
  },

  "tapToAddItems": "Tap + to add items",
  "@tapToAddItems": {
    "description": "Hint to add items"
  },

  "itemQuantity": "{quantity}x in {category}",
  "@itemQuantity": {
    "description": "Item quantity and category",
    "placeholders": {
      "quantity": {
        "type": "int"
      },
      "category": {
        "type": "String"
      }
    }
  },

  "decreaseQuantity": "Decrease quantity",
  "@decreaseQuantity": {
    "description": "Decrease quantity tooltip"
  },

  "increaseQuantity": "Increase quantity",
  "@increaseQuantity": {
    "description": "Increase quantity tooltip"
  },

  "addItemToCategory": "Add item to {category}",
  "@addItemToCategory": {
    "description": "Dialog title for adding item",
    "placeholders": {
      "category": {
        "type": "String"
      }
    }
  },

  "itemNameLabel": "Item name",
  "@itemNameLabel": {
    "description": "Label for item name field"
  },

  "itemNameHint": "Enter item name",
  "@itemNameHint": {
    "description": "Hint for item name field"
  },

  "cancelAction": "Cancel",
  "@cancelAction": {
    "description": "Cancel action label"
  },

  "addAction": "Add",
  "@addAction": {
    "description": "Add action label"
  },

  "itemAddedToCategory": "{item} added to {category}",
  "@itemAddedToCategory": {
    "description": "Message when item added to category",
    "placeholders": {
      "item": {
        "type": "String"
      },
      "category": {
        "type": "String"
      }
    }
  },

  "priorityTasksTitle": "Priority Tasks",
  "@priorityTasksTitle": {
    "description": "Title for priority tasks"
  },

  "sortByPriorityTooltip": "Sort by priority",
  "@sortByPriorityTooltip": {
    "description": "Tooltip for sort button"
  },

  "noTasksMessage": "No tasks yet",
  "@noTasksMessage": {
    "description": "Empty tasks message"
  },

  "taskReviewCode": "Review pull request",
  "@taskReviewCode": {
    "description": "Sample task"
  },

  "taskWriteTests": "Write unit tests",
  "@taskWriteTests": {
    "description": "Sample task"
  },

  "taskUpdateDocs": "Update documentation",
  "@taskUpdateDocs": {
    "description": "Sample task"
  },

  "taskDeployApp": "Deploy to production",
  "@taskDeployApp": {
    "description": "Sample task"
  },

  "priorityLevel": "Priority {level}",
  "@priorityLevel": {
    "description": "Priority level label",
    "placeholders": {
      "level": {
        "type": "int"
      }
    }
  },

  "deleteTaskTooltip": "Delete task",
  "@deleteTaskTooltip": {
    "description": "Delete task tooltip"
  },

  "taskReordered": "{task} moved to position {position}",
  "@taskReordered": {
    "description": "Task reordered message",
    "placeholders": {
      "task": {
        "type": "String"
      },
      "position": {
        "type": "int"
      }
    }
  },

  "taskRemoved": "{task} removed",
  "@taskRemoved": {
    "description": "Task removed message",
    "placeholders": {
      "task": {
        "type": "String"
      }
    }
  },

  "tasksSortedByPriority": "Tasks sorted by priority",
  "@tasksSortedByPriority": {
    "description": "Tasks sorted message"
  },

  "addNewTask": "Add new task",
  "@addNewTask": {
    "description": "Add new task dialog title"
  },

  "taskTitleLabel": "Task title",
  "@taskTitleLabel": {
    "description": "Task title label"
  },

  "taskTitleHint": "Enter task title",
  "@taskTitleHint": {
    "description": "Task title hint"
  },

  "addTaskTooltip": "Add task",
  "@addTaskTooltip": {
    "description": "Add task tooltip"
  },

  "taskAdded": "{task} added",
  "@taskAdded": {
    "description": "Task added message",
    "placeholders": {
      "task": {
        "type": "String"
      }
    }
  },

  "notificationsTitle": "Notifications",
  "@notificationsTitle": {
    "description": "Notifications screen title"
  },

  "notificationCount": "{count, plural, =0{No notifications} =1{1 notification} other{{count} notifications}}",
  "@notificationCount": {
    "description": "Notification count",
    "placeholders": {
      "count": {
        "type": "int"
      }
    }
  },

  "noNotifications": "No notifications",
  "@noNotifications": {
    "description": "No notifications message"
  },

  "noNotificationsAccessibilityLabel": "You have no notifications",
  "@noNotificationsAccessibilityLabel": {
    "description": "Accessibility label for empty state"
  },

  "simulateNotificationTooltip": "Simulate new notification",
  "@simulateNotificationTooltip": {
    "description": "Tooltip for simulate button"
  },

  "notificationAccessibilityLabel": "{title}, received {time}",
  "@notificationAccessibilityLabel": {
    "description": "Accessibility label for notification",
    "placeholders": {
      "title": {
        "type": "String"
      },
      "time": {
        "type": "String"
      }
    }
  },

  "notificationHint": "Double tap to mark as read, swipe to dismiss",
  "@notificationHint": {
    "description": "Accessibility hint for notification"
  },

  "dismissAction": "Dismiss",
  "@dismissAction": {
    "description": "Dismiss action label"
  },

  "markAsReadAction": "Mark as read",
  "@markAsReadAction": {
    "description": "Mark as read action label"
  },

  "notificationIconLabel": "{type} notification",
  "@notificationIconLabel": {
    "description": "Icon accessibility label",
    "placeholders": {
      "type": {
        "type": "String"
      }
    }
  },

  "dismissNotification": "Dismiss notification",
  "@dismissNotification": {
    "description": "Dismiss notification tooltip"
  },

  "notificationTitle": "New {type}",
  "@notificationTitle": {
    "description": "Notification title template",
    "placeholders": {
      "type": {
        "type": "String"
      }
    }
  },

  "justNow": "Just now",
  "@justNow": {
    "description": "Just now time label"
  },

  "newNotificationAnnouncement": "New notification: {title}",
  "@newNotificationAnnouncement": {
    "description": "Screen reader announcement for new notification",
    "placeholders": {
      "title": {
        "type": "String"
      }
    }
  },

  "notificationDismissed": "{title} dismissed",
  "@notificationDismissed": {
    "description": "Notification dismissed announcement",
    "placeholders": {
      "title": {
        "type": "String"
      }
    }
  },

  "notificationMarkedAsRead": "Notification marked as read",
  "@notificationMarkedAsRead": {
    "description": "Notification read announcement"
  }
}

Testing AnimatedList Localization

Comprehensive tests for animated list:

import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';

void main() {
  group('AnimatedList Localization Tests', () {
    testWidgets('displays localized empty state', (tester) async {
      await tester.pumpWidget(
        MaterialApp(
          localizationsDelegates: AppLocalizations.localizationsDelegates,
          supportedLocales: AppLocalizations.supportedLocales,
          locale: const Locale('es'),
          home: const LocalizedAnimatedList(),
        ),
      );

      // Verify Spanish empty state
      expect(find.text('Tu lista esta vacia'), findsOneWidget);
    });

    testWidgets('shows localized item added snackbar', (tester) async {
      await tester.pumpWidget(
        MaterialApp(
          localizationsDelegates: AppLocalizations.localizationsDelegates,
          supportedLocales: AppLocalizations.supportedLocales,
          locale: const Locale('fr'),
          home: const LocalizedAnimatedList(),
        ),
      );

      // Tap add button
      await tester.tap(find.byType(FloatingActionButton));
      await tester.pumpAndSettle();

      // Verify French feedback
      expect(find.textContaining('ajoute a la liste'), findsOneWidget);
    });

    testWidgets('animates item insertion', (tester) async {
      await tester.pumpWidget(
        MaterialApp(
          localizationsDelegates: AppLocalizations.localizationsDelegates,
          supportedLocales: AppLocalizations.supportedLocales,
          home: const LocalizedAnimatedList(),
        ),
      );

      // Add item
      await tester.tap(find.byType(FloatingActionButton));
      await tester.pump();

      // Verify animation is running
      expect(find.byType(SizeTransition), findsOneWidget);

      await tester.pumpAndSettle();
    });

    testWidgets('handles RTL layout', (tester) async {
      await tester.pumpWidget(
        MaterialApp(
          localizationsDelegates: AppLocalizations.localizationsDelegates,
          supportedLocales: AppLocalizations.supportedLocales,
          locale: const Locale('ar'),
          home: const Directionality(
            textDirection: TextDirection.rtl,
            child: LocalizedAnimatedList(),
          ),
        ),
      );

      await tester.tap(find.byType(FloatingActionButton));
      await tester.pumpAndSettle();

      // Items should be rendered in RTL
      expect(find.byType(ListTile), findsWidgets);
    });

    testWidgets('accessibility announcements work', (tester) async {
      final SemanticsHandle handle = tester.ensureSemantics();

      await tester.pumpWidget(
        MaterialApp(
          localizationsDelegates: AppLocalizations.localizationsDelegates,
          supportedLocales: AppLocalizations.supportedLocales,
          home: const AccessibleAnimatedList(),
        ),
      );

      // Verify semantics are present
      expect(
        tester.getSemantics(find.byType(Semantics).first),
        matchesSemantics(label: contains('notification')),
      );

      handle.dispose();
    });
  });
}

Best Practices Summary

  1. Announce list changes: Use SemanticsService.announce to inform screen reader users of additions and removals

  2. Provide undo options: Always allow users to reverse accidental deletions with localized undo actions

  3. Localize empty states: Empty list messages should be encouraging and localized

  4. Handle pluralization: Use proper plural forms for item counts in all languages

  5. Animate thoughtfully: Keep animations short enough that localized feedback remains readable

  6. Support keyboard navigation: Provide alternative ways to add/remove items for keyboard users

  7. Maintain list identity: Use stable keys for AnimatedList items to ensure proper animations

  8. Test animation timing: Verify animations work well with text of varying lengths across languages

By following these patterns, your AnimatedList widgets will provide smooth, accessible, and properly localized experiences for all users.