[{"data":1,"prerenderedAt":78},["ShallowReactive",2],{"tutorial-liste-animate-in-flutter-con-animatedlist-e-sliveranimatedlist":3,"comments-tutorial-liste-animate-in-flutter-con-animatedlist-e-sliveranimatedlist":77},{"id":4,"title":5,"slug":6,"excerpt":7,"intro":8,"cover_image":9,"cover_remote_url":10,"cover_credit":11,"video_url":15,"video":16,"difficulty":20,"estimated_minutes":21,"flutter_version":22,"status":23,"published_at":24,"meta_title":25,"meta_description":26,"category":27,"author":31,"steps":33},121,"Liste animate in Flutter con AnimatedList e SliverAnimatedList","liste-animate-in-flutter-con-animatedlist-e-sliveranimatedlist","Impara a costruire liste che animano inserimenti e rimozioni con AnimatedList, un ListModel sincronizzato, lo snackbar di undo e la versione sliver dentro un CustomScrollView.","Quando aggiungi o rimuovi un elemento da una `ListView`, Flutter ridisegna la lista di colpo: niente transizioni, l'utente perde il filo di cosa è cambiato.\n\n`AnimatedList` (e la sua controparte `SliverAnimatedList`) risolve il problema gestendo un **conteggio interno di elementi** che puoi far crescere o diminuire con animazione tramite `insertItem` e `removeItem`.\n\nIl punto delicato — e il motivo per cui molti sbagliano — è che `AnimatedList` **non osserva la tua lista di dati**: devi tenere sincronizzati manualmente la sorgente dati e lo stato del widget. In questa guida costruiamo una piccola app di task in cui:\n\n- incapsuliamo la sincronizzazione in una classe riutilizzabile `ListModel`;\n- animiamo inserimenti singoli e multipli;\n- animiamo le rimozioni con undo tramite `SnackBar`;\n- portiamo tutto dentro un `CustomScrollView` con `SliverAnimatedList` e `SliverAppBar`.\n\nServono conoscenze di base su `StatefulWidget`, `GlobalKey` e animazioni implicite\u002Fesplicite.","https:\u002F\u002Fflutter.it\u002Fstorage\u002Ftutorials\u002F46f3a934-6871-4b2d-912d-c52d81285315.jpg","https:\u002F\u002Fimages.unsplash.com\u002Fphoto-1636033503567-a59bff19d79a?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w5NzA2NTJ8MHwxfHJhbmRvbXx8fHx8fHx8fDE3OTE2OTMwOTd8&ixlib=rb-4.1.0&q=80&w=1080",{"name":12,"author_url":13,"photo_url":14},"Roman","https:\u002F\u002Funsplash.com\u002F@timesnewroman14","https:\u002F\u002Funsplash.com\u002Fphotos\u002Fa-calculator-sitting-on-top-of-a-table-DEga33BYvmA","https:\u002F\u002Fwww.youtube.com\u002Fwatch?v=CN8lOEaTTXA",{"youtube_id":17,"duration_seconds":18,"published_at":19},"CN8lOEaTTXA",130,"2026-10-11T11:07:35+00:00","intermediate",35,"3.x","published","2026-10-11T04:31:37+00:00","AnimatedList in Flutter: inserimenti e rimozioni animate","Guida pratica a AnimatedList e SliverAnimatedList in Flutter: ListModel sincronizzato, insertItem, removeItem, undo con SnackBar e integrazione negli sliver.",{"id":28,"name":29,"slug":30},1,"Guide","guide",{"id":28,"name":32},"Flutter Bot",[34,42,49,56,63,70],{"id":35,"position":28,"title":36,"body":37,"code_snippet":38,"code_language":39,"expected_result":40,"demo_url":41,"video_url":41},809,"Modello dati e classe ListModel per sincronizzare dati e widget","`AnimatedList` mantiene internamente solo il **numero** di elementi da mostrare. Se rimuovi un oggetto dalla tua `List\u003CTask>` senza chiamare `removeItem`, il widget continuerà a chiedere un elemento che non esiste più → `RangeError`.\n\nLa soluzione pulita è incapsulare la lista reale e la `GlobalKey\u003CAnimatedListState>` in un'unica classe: ogni mutazione passa da lì e aggiorna entrambi.\n\nCrea `lib\u002Flist_model.dart` con il modello `Task` e la classe generica `ListModel\u003CE>`.\n\nNota il parametro `removedItemBuilder`: quando un elemento viene rimosso dai dati, il widget deve comunque essere disegnato per la durata dell'animazione di uscita, quindi serve un builder che sappia ricostruirlo a partire dall'oggetto già estratto.","import 'package:flutter\u002Fmaterial.dart';\n\nclass Task {\n  const Task({required this.id, required this.title});\n\n  final int id;\n  final String title;\n}\n\ntypedef RemovedItemBuilder\u003CE> = Widget Function(\n  E item,\n  BuildContext context,\n  Animation\u003Cdouble> animation,\n);\n\n\u002F\u002F\u002F Tiene allineati la lista dei dati e lo stato di AnimatedList.\nclass ListModel\u003CE> {\n  ListModel({\n    required this.listKey,\n    required this.removedItemBuilder,\n    Iterable\u003CE>? initialItems,\n  }) : _items = List\u003CE>.from(initialItems ?? \u003CE>[]);\n\n  final GlobalKey\u003CAnimatedListState> listKey;\n  final RemovedItemBuilder\u003CE> removedItemBuilder;\n  final List\u003CE> _items;\n\n  static const Duration _duration = Duration(milliseconds: 350);\n\n  AnimatedListState get _animatedList => listKey.currentState!;\n\n  int get length => _items.length;\n  bool get isEmpty => _items.isEmpty;\n  E operator [](int index) => _items[index];\n  int indexOf(E item) => _items.indexOf(item);\n\n  void insert(int index, E item) {\n    _items.insert(index, item);\n    _animatedList.insertItem(index, duration: _duration);\n  }\n\n  void insertAll(int index, List\u003CE> items) {\n    _items.insertAll(index, items);\n    _animatedList.insertAllItems(index, items.length, duration: _duration);\n  }\n\n  E removeAt(int index) {\n    final E removed = _items.removeAt(index);\n    _animatedList.removeItem(\n      index,\n      (context, animation) => removedItemBuilder(removed, context, animation),\n      duration: _duration,\n    );\n    return removed;\n  }\n}","dart","Hai una classe `ListModel` riutilizzabile: ogni inserimento\u002Frimozione aggiorna sia i dati sia il widget, senza bisogno di `setState` sulla lista.",null,{"id":43,"position":44,"title":45,"body":46,"code_snippet":47,"code_language":39,"expected_result":48,"demo_url":41,"video_url":41},810,2,"Costruire la schermata con AnimatedList","Ora creiamo lo `StatefulWidget` che ospita la lista. Punti chiave:\n\n- la `GlobalKey\u003CAnimatedListState>` va creata **una sola volta** in `initState` (o come campo `final`), mai nel `build`;\n- `initialItemCount` deve coincidere con il numero di elementi iniziali del modello;\n- l'`itemBuilder` riceve un `Animation\u003Cdouble>` che va applicato al widget: tipicamente `SizeTransition` (per far \"crescere\" lo spazio) combinata con `FadeTransition`.\n\nIsoliamo la riga in un widget `_TaskTile` così da riutilizzarlo sia per gli elementi vivi sia per quelli in uscita.","import 'package:flutter\u002Fmaterial.dart';\nimport 'list_model.dart';\n\nclass TaskListPage extends StatefulWidget {\n  const TaskListPage({super.key});\n\n  @override\n  State\u003CTaskListPage> createState() => _TaskListPageState();\n}\n\nclass _TaskListPageState extends State\u003CTaskListPage> {\n  final GlobalKey\u003CAnimatedListState> _listKey = GlobalKey\u003CAnimatedListState>();\n  late final ListModel\u003CTask> _tasks;\n  int _nextId = 0;\n\n  @override\n  void initState() {\n    super.initState();\n    final initial = List\u003CTask>.generate(\n      3,\n      (i) => Task(id: _nextId++, title: 'Task iniziale ${i + 1}'),\n    );\n    _tasks = ListModel\u003CTask>(\n      listKey: _listKey,\n      initialItems: initial,\n      removedItemBuilder: _buildRemovedItem,\n    );\n  }\n\n  Widget _buildRemovedItem(\n    Task task,\n    BuildContext context,\n    Animation\u003Cdouble> animation,\n  ) {\n    return _TaskTile(task: task, animation: animation);\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return Scaffold(\n      appBar: AppBar(title: const Text('AnimatedList')),\n      body: AnimatedList(\n        key: _listKey,\n        initialItemCount: _tasks.length,\n        padding: const EdgeInsets.symmetric(vertical: 8),\n        itemBuilder: (context, index, animation) {\n          return _TaskTile(\n            task: _tasks[index],\n            animation: animation,\n          );\n        },\n      ),\n    );\n  }\n}\n\nclass _TaskTile extends StatelessWidget {\n  const _TaskTile({\n    required this.task,\n    required this.animation,\n    this.onDelete,\n  });\n\n  final Task task;\n  final Animation\u003Cdouble> animation;\n  final VoidCallback? onDelete;\n\n  @override\n  Widget build(BuildContext context) {\n    final curved = CurvedAnimation(\n      parent: animation,\n      curve: Curves.easeOutCubic,\n    );\n    return SizeTransition(\n      sizeFactor: curved,\n      child: FadeTransition(\n        opacity: curved,\n        child: Card(\n          margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 6),\n          child: ListTile(\n            leading: CircleAvatar(child: Text('${task.id}')),\n            title: Text(task.title),\n            trailing: onDelete == null\n                ? null\n                : IconButton(\n                    icon: const Icon(Icons.delete_outline),\n                    onPressed: onDelete,\n                  ),\n          ),\n        ),\n      ),\n    );\n  }\n}","L'app mostra tre card statiche. Non ci sono ancora animazioni visibili perché nessun elemento viene aggiunto o rimosso.",{"id":50,"position":51,"title":52,"body":53,"code_snippet":54,"code_language":39,"expected_result":55,"demo_url":41,"video_url":41},811,3,"Inserire elementi con animazione (singoli e multipli)","Aggiungiamo un `FloatingActionButton` che inserisce un task **in cima** alla lista e un pulsante in AppBar che ne inserisce tre in un colpo solo con `insertAllItems`.\n\nDettaglio importante: non serve `setState` per aggiungere elementi, perché `insertItem` notifica direttamente l'`AnimatedListState`. Usa `setState` solo se cambia altro stato della pagina (es. un contatore mostrato nell'AppBar).\n\nSe vuoi che la lista scorra fino al nuovo elemento, collega uno `ScrollController` e chiama `animateTo` dopo l'inserimento.","  void _addTask() {\n    final task = Task(id: _nextId, title: 'Nuovo task $_nextId');\n    _nextId++;\n    _tasks.insert(0, task); \u002F\u002F inserisce in testa con animazione\n  }\n\n  void _addBatch() {\n    final batch = List\u003CTask>.generate(\n      3,\n      (i) => Task(id: _nextId + i, title: 'Batch ${_nextId + i}'),\n    );\n    _nextId += batch.length;\n    _tasks.insertAll(_tasks.length, batch); \u002F\u002F in coda, tutti insieme\n  }\n\n  \u002F\u002F Nel build dello Scaffold:\n  \u002F\u002F appBar: AppBar(\n  \u002F\u002F   title: const Text('AnimatedList'),\n  \u002F\u002F   actions: [\n  \u002F\u002F     IconButton(\n  \u002F\u002F       icon: const Icon(Icons.playlist_add),\n  \u002F\u002F       onPressed: _addBatch,\n  \u002F\u002F     ),\n  \u002F\u002F   ],\n  \u002F\u002F ),\n  \u002F\u002F floatingActionButton: FloatingActionButton(\n  \u002F\u002F   onPressed: _addTask,\n  \u002F\u002F   child: const Icon(Icons.add),\n  \u002F\u002F ),","Premendo il FAB una nuova card compare in testa espandendosi e dissolvendosi in entrata; il pulsante in AppBar aggiunge tre card in coda contemporaneamente.",{"id":57,"position":58,"title":59,"body":60,"code_snippet":61,"code_language":39,"expected_result":62,"demo_url":41,"video_url":41},812,4,"Rimuovere elementi con animazione e undo tramite SnackBar","La rimozione è l'operazione più insidiosa: l'elemento deve sparire dai dati **subito** (altrimenti l'indice si disallinea) ma restare visibile fino a fine animazione. È esattamente ciò che fa `ListModel.removeAt`, grazie al `removedItemBuilder`.\n\nDue regole d'oro:\n\n1. calcola l'indice **al momento del tap**, con `indexOf`, non salvare indici in variabili di lungo periodo;\n2. nel widget in uscita passa `onDelete: null`, così l'utente non può premere due volte \"elimina\" sullo stesso elemento durante l'animazione.\n\nAggiungiamo anche l'undo: ri-inseriamo l'elemento nella stessa posizione, ottenendo l'animazione inversa gratis.","  void _removeTask(Task task) {\n    final index = _tasks.indexOf(task);\n    if (index \u003C 0) return; \u002F\u002F già rimosso\n    final removed = _tasks.removeAt(index);\n\n    ScaffoldMessenger.of(context)\n      ..hideCurrentSnackBar()\n      ..showSnackBar(\n        SnackBar(\n          content: Text('\"${removed.title}\" eliminato'),\n          action: SnackBarAction(\n            label: 'ANNULLA',\n            onPressed: () => _tasks.insert(\n              index.clamp(0, _tasks.length),\n              removed,\n            ),\n          ),\n        ),\n      );\n  }\n\n  \u002F\u002F itemBuilder aggiornato:\n  \u002F\u002F itemBuilder: (context, index, animation) {\n  \u002F\u002F   final task = _tasks[index];\n  \u002F\u002F   return _TaskTile(\n  \u002F\u002F     task: task,\n  \u002F\u002F     animation: animation,\n  \u002F\u002F     onDelete: () => _removeTask(task),\n  \u002F\u002F   );\n  \u002F\u002F },","Toccando l'icona cestino la card collassa e svanisce; la SnackBar permette di ripristinarla nella stessa posizione con animazione di entrata.",{"id":64,"position":65,"title":66,"body":67,"code_snippet":68,"code_language":39,"expected_result":69,"demo_url":41,"video_url":41},813,5,"Portare l'animazione dentro un CustomScrollView con SliverAnimatedList","Se la lista deve convivere con una `SliverAppBar` collassabile o con altri sliver, `AnimatedList` non basta: è un widget scrollabile a sé. Usa `SliverAnimatedList`, che ha la stessa API ma vive dentro un `CustomScrollView`.\n\nLe differenze pratiche sono due:\n\n- la chiave diventa `GlobalKey\u003CSliverAnimatedListState>`;\n- il padding si applica con `SliverPadding`, non con il parametro `padding`.\n\nPer riusare il `ListModel` basta renderlo generico sul tipo di stato oppure, come qui, creare una variante che accetta una chiave sliver. In questo esempio duplichiamo solo le poche righe necessarie.","class SliverTaskListPage extends StatefulWidget {\n  const SliverTaskListPage({super.key});\n\n  @override\n  State\u003CSliverTaskListPage> createState() => _SliverTaskListPageState();\n}\n\nclass _SliverTaskListPageState extends State\u003CSliverTaskListPage> {\n  final _listKey = GlobalKey\u003CSliverAnimatedListState>();\n  final List\u003CTask> _items = List\u003CTask>.generate(\n    5,\n    (i) => Task(id: i, title: 'Task $i'),\n  );\n\n  void _remove(Task task) {\n    final index = _items.indexOf(task);\n    if (index \u003C 0) return;\n    final removed = _items.removeAt(index);\n    _listKey.currentState!.removeItem(\n      index,\n      (context, animation) => _TaskTile(task: removed, animation: animation),\n      duration: const Duration(milliseconds: 350),\n    );\n  }\n\n  void _add() {\n    final task = Task(id: _items.length, title: 'Task ${_items.length}');\n    _items.insert(0, task);\n    _listKey.currentState!.insertItem(\n      0,\n      duration: const Duration(milliseconds: 350),\n    );\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return Scaffold(\n      floatingActionButton: FloatingActionButton(\n        onPressed: _add,\n        child: const Icon(Icons.add),\n      ),\n      body: CustomScrollView(\n        slivers: [\n          const SliverAppBar.large(title: Text('Sliver animata')),\n          SliverPadding(\n            padding: const EdgeInsets.only(bottom: 80),\n            sliver: SliverAnimatedList(\n              key: _listKey,\n              initialItemCount: _items.length,\n              itemBuilder: (context, index, animation) {\n                final task = _items[index];\n                return _TaskTile(\n                  task: task,\n                  animation: animation,\n                  onDelete: () => _remove(task),\n                );\n              },\n            ),\n          ),\n        ],\n      ),\n    );\n  }\n}","La lista animata scorre insieme alla SliverAppBar che si collassa, mantenendo le stesse transizioni di entrata e uscita.",{"id":71,"position":72,"title":73,"body":74,"code_snippet":75,"code_language":39,"expected_result":76,"demo_url":41,"video_url":41},814,6,"Errori comuni, varianti e best practice","Prima di portare `AnimatedList` in produzione, tieni a mente questi punti.\n\n**Errori frequenti**\n\n- *`Null check operator used on a null value`* su `listKey.currentState`: stai mutando la lista prima che il widget sia montato (es. nel `initState` o in un callback arrivato dopo il `dispose`). Proteggi con `final state = listKey.currentState; if (state == null) return;`.\n- *`RangeError (index)`*: hai modificato la `List` sorgente senza passare da `insertItem`\u002F`removeItem`. Rendi la lista privata e esponi solo i metodi del `ListModel`.\n- *Due chiavi uguali*: non riusare la stessa `GlobalKey` in due `AnimatedList` contemporaneamente (es. in un `TabBarView` che li costruisce entrambi).\n- *`Dismissible` dentro `AnimatedList`*: funziona solo se in `onDismissed` rimuovi l'elemento con `removeItem` passando un builder che restituisce `SizedBox.shrink()`, altrimenti vedrai la riga \"tornare\" per un istante.\n\n**Varianti utili**\n\n- `AnimatedGrid` \u002F `SliverAnimatedGrid`: stessa logica per le griglie.\n- `AnimatedList.separated` (dalle versioni recenti di Flutter 3.x) aggiunge i separatori animati senza dover gestire indici pari\u002Fdispari.\n- Se preferisci un approccio dichiarativo in cui passi la lista e il diff viene calcolato per te, valuta pacchetti come `animated_list_plus` o usa `AnimatedSwitcher` per liste molto corte.\n\n**Performance**: mantieni gli item `const` dove possibile, evita di ricostruire l'intera pagina con `setState` a ogni mutazione e assegna una `ValueKey` stabile alle righe se contengono stato interno.\n\nCome esercizio finale, aggiungi un pulsante \"Svuota\" che rimuove tutti gli elementi uno alla volta con un piccolo ritardo, per ottenere un effetto a cascata.","  Future\u003Cvoid> _clearWithStagger() async {\n    while (!_tasks.isEmpty) {\n      _tasks.removeAt(_tasks.length - 1);\n      await Future\u003Cvoid>.delayed(const Duration(milliseconds: 80));\n      if (!mounted) return; \u002F\u002F il widget potrebbe essere stato smontato\n    }\n  }\n\n  \u002F\u002F Rimozione istantanea di tutti gli elementi (senza animazione di uscita):\n  \u002F\u002F _listKey.currentState?.removeAllItems(\n  \u002F\u002F   (context, animation) => const SizedBox.shrink(),\n  \u002F\u002F   duration: Duration.zero,\n  \u002F\u002F );","Premendo \"Svuota\" le card spariscono una dopo l'altra con effetto a cascata, senza eccezioni anche se si lascia la pagina durante l'animazione.",[],1792316904541]