[{"data":1,"prerenderedAt":27},["ShallowReactive",2],{"articolo-le-key-in-flutter-cosa-sono-davvero-e-quando-servono":3,"comments-article-le-key-in-flutter-cosa-sono-davvero-e-quando-servono":26},{"id":4,"title":5,"slug":6,"excerpt":7,"body":8,"cover_image":9,"cover_remote_url":10,"cover_credit":11,"video_url":15,"status":16,"published_at":17,"meta_title":18,"meta_description":19,"category":20,"author":24},84,"Le Key in Flutter: cosa sono davvero e quando servono","le-key-in-flutter-cosa-sono-davvero-e-quando-servono","ValueKey, ObjectKey, UniqueKey, PageStorageKey e GlobalKey: come Flutter usa le key per riconciliare l'albero dei widget e come evitare bug con stato, liste riordinabili e AnimatedSwitcher.","## Perché le Key esistono\n\nPrima o poi capita a tutti: si riordina una lista, si rimuove un elemento e improvvisamente lo stato \"salta\" sul widget sbagliato. Un checkbox spuntato finisce sulla riga vicina, un `TextField` mantiene il testo di un altro item, un'animazione parte quando non dovrebbe. Nella quasi totalità dei casi la causa è la stessa: mancano le **Key**.\n\nPer capirle bisogna ricordare che in Flutter esistono tre alberi paralleli:\n\n- l'**albero dei Widget**: configurazioni immutabili, ricreate a ogni build, economiche da costruire;\n- l'**albero degli Element**: la struttura persistente che collega widget e render object e che **contiene lo State** degli `StatefulWidget`;\n- l'**albero dei RenderObject**: quello che si occupa di layout e painting.\n\nQuando il framework ricostruisce una porzione di UI, non butta via tutto: confronta il nuovo widget con quello vecchio nella stessa posizione e decide se può **riusare l'Element esistente**. La regola è implementata in `Widget.canUpdate`:\n\n```dart\nstatic bool canUpdate(Widget oldWidget, Widget newWidget) {\n  return oldWidget.runtimeType == newWidget.runtimeType\n      && oldWidget.key == newWidget.key;\n}\n```\n\nStesso tipo e stessa key (anche `null == null`) ⇒ l'Element viene aggiornato e lo State sopravvive. Altrimenti il vecchio Element viene smontato e ne viene creato uno nuovo, perdendo lo stato.\n\nIl punto chiave è che il confronto avviene **per posizione nella lista dei figli**. Se non ci sono key e gli elementi si spostano, Flutter è convinto che il primo figlio nuovo corrisponda al primo figlio vecchio: da qui i bug.\n\n## Il caso classico: una lista che si rimescola\n\nCreiamo un widget con stato interno, senza key:\n\n```dart\nclass CounterTile extends StatefulWidget {\n  const CounterTile({required this.label, super.key});\n  final String label;\n\n  @override\n  State\u003CCounterTile> createState() => _CounterTileState();\n}\n\nclass _CounterTileState extends State\u003CCounterTile> {\n  int _count = 0;\n\n  @override\n  Widget build(BuildContext context) {\n    return ListTile(\n      title: Text(widget.label),\n      trailing: Text('$_count'),\n      onTap: () => setState(() => _count++),\n    );\n  }\n}\n```\n\nE una schermata che permette di invertire l'ordine:\n\n```dart\nclass DemoPage extends StatefulWidget {\n  const DemoPage({super.key});\n  @override\n  State\u003CDemoPage> createState() => _DemoPageState();\n}\n\nclass _DemoPageState extends State\u003CDemoPage> {\n  List\u003CString> items = ['Alpha', 'Beta', 'Gamma'];\n\n  @override\n  Widget build(BuildContext context) {\n    return Scaffold(\n      appBar: AppBar(\n        actions: [\n          IconButton(\n            icon: const Icon(Icons.swap_vert),\n            onPressed: () => setState(() => items = items.reversed.toList()),\n          ),\n        ],\n      ),\n      body: Column(\n        children: [\n          for (final item in items) CounterTile(label: item),\n        ],\n      ),\n    );\n  }\n}\n```\n\nIncrementate il contatore di \"Alpha\", poi invertite l'ordine: il valore resta in cima, attaccato alla posizione e non all'elemento. Aggiungendo una key stabile il problema sparisce:\n\n```dart\nfor (final item in items)\n  CounterTile(key: ValueKey(item), label: item),\n```\n\nOra Flutter riconosce l'Element di \"Alpha\" anche se si è spostato e lo riordina invece di ricrearlo.\n\n> Nota: con i `StatelessWidget` senza figli stateful il problema non si vede, perché non c'è stato da preservare. Le key contano quando c'è **stato** (State, controller, animazioni, scroll position) o quando servono animazioni corrette in liste dinamiche.\n\n## Dove va messa la key\n\nErrore frequentissimo: mettere la key **dentro** il widget che si sposta invece che sulla sua radice.\n\n```dart\n\u002F\u002F ❌ inutile: il ListTile interno è già figlio di un CounterTile senza key\nListTile(key: ValueKey(item), ...)\n\n\u002F\u002F ✅ la key va sul widget più in alto del sottoalbero che cambia posizione\nCounterTile(key: ValueKey(item), label: item)\n```\n\nLa regola pratica: **la key va assegnata al nodo più alto del sottoalbero che può spostarsi, essere rimosso o riordinato tra un frame e l'altro**.\n\n## I tipi di key e quando usarli\n\n### ValueKey\n\nUsa un valore con `==`\u002F`hashCode` significativi: l'id di un record, uno slug, un enum.\n\n```dart\nValueKey(todo.id)\n```\n\nÈ la scelta di default. Attenzione a non usare l'**indice** della lista come valore: `ValueKey(index)` è equivalente a non avere key, perché l'indice cambia quando la lista si riordina.\n\n### ObjectKey\n\nConfronta per **identità** dell'oggetto (`identical`), non per uguaglianza di valore. Utile quando gli oggetti non hanno un id univoco ma le istanze sono stabili nel tempo.\n\n```dart\nObjectKey(person) \u002F\u002F due persone omonime restano distinte\n```\n\n### UniqueKey\n\nOgni istanza è diversa da qualunque altra. Serve quando si vuole **forzare** la ricostruzione di un sottoalbero, non per identificare elementi in una lista: creata in `build`, cambierebbe a ogni frame distruggendo lo stato.\n\n```dart\n\u002F\u002F forza il reset completo del player quando cambia sorgente\nVideoWidget(key: UniqueKey(), url: url)\n```\n\n### PageStorageKey\n\nSpecializzata nel salvare e ripristinare valori (tipicamente la posizione di scroll) tramite `PageStorage`. Indispensabile con `TabBarView`, `IndexedStack` o `NestedScrollView`, dove le liste vengono smontate e rimontate:\n\n```dart\nListView.builder(\n  key: const PageStorageKey\u003CString>('feed_list'),\n  itemCount: posts.length,\n  itemBuilder: (context, i) => PostCard(post: posts[i]),\n)\n```\n\n### GlobalKey\n\nÈ una categoria a parte: identifica un widget **in tutta l'app**, dà accesso al suo `State`, al suo `BuildContext` e al suo `RenderObject`, e permette di spostare un sottoalbero in un punto diverso dell'albero mantenendo lo stato (GlobalKey reparenting).\n\nL'uso più comune resta la validazione di un form:\n\n```dart\nfinal _formKey = GlobalKey\u003CFormState>();\n\nForm(\n  key: _formKey,\n  child: Column(\n    children: [\n      TextFormField(\n        validator: (v) => (v == null || v.isEmpty) ? 'Campo obbligatorio' : null,\n      ),\n      ElevatedButton(\n        onPressed: () {\n          if (_formKey.currentState!.validate()) {\n            _formKey.currentState!.save();\n          }\n        },\n        child: const Text('Invia'),\n      ),\n    ],\n  ),\n)\n```\n\nUn altro uso legittimo è misurare un widget dopo il layout:\n\n```dart\nfinal _boxKey = GlobalKey();\n\nvoid _measure() {\n  final box = _boxKey.currentContext?.findRenderObject() as RenderBox?;\n  if (box != null) {\n    debugPrint('Dimensioni: ${box.size}');\n  }\n}\n```\n\n**Costi e insidie delle GlobalKey**:\n\n- sono più pesanti delle key locali (registro globale, gestione del reparenting);\n- una GlobalKey **non può comparire due volte** nello stesso albero: lo stesso widget renderizzato in due punti provoca un'eccezione;\n- vanno create come campo dello `State` (`final`), **mai dentro `build`**;\n- usarle per far comunicare widget lontani è un anti-pattern: per quello esistono `InheritedWidget`, provider, BLoC o Riverpod.\n\n## Key e animazioni: AnimatedSwitcher, AnimatedList, Hero\n\n`AnimatedSwitcher` decide se animare proprio in base a `canUpdate`. Se il widget nuovo ha lo stesso tipo e nessuna key, la transizione non parte:\n\n```dart\nAnimatedSwitcher(\n  duration: const Duration(milliseconds: 300),\n  child: Text(\n    '$counter',\n    key: ValueKey\u003Cint>(counter), \u002F\u002F senza key non c'è cross-fade\n    style: Theme.of(context).textTheme.headlineMedium,\n  ),\n)\n```\n\nLo stesso vale per `AnimatedList`, `ReorderableListView` (che **richiede** key univoche e stabili su ogni figlio) e per le transizioni Hero tra pagine:\n\n```dart\nReorderableListView(\n  onReorder: _onReorder,\n  children: [\n    for (final task in tasks)\n      TaskTile(key: ValueKey(task.id), task: task),\n  ],\n)\n```\n\n## Un caso subdolo: due branch dello stesso tipo\n\n```dart\n\u002F\u002F ❌ passando da editing a lettura, il TextField mantiene lo stato\nisEditing\n  ? TextField(controller: c)\n  : TextField(controller: c, readOnly: true)\n```\n\nQui i due widget hanno lo stesso `runtimeType` e nessuna key: l'Element viene riusato, con effetti collaterali su focus e selezione. Basta differenziarli:\n\n```dart\nisEditing\n  ? TextField(key: const ValueKey('edit'), controller: c)\n  : TextField(key: const ValueKey('read'), controller: c, readOnly: true)\n```\n\nAl contrario, se **volete** che lo stato sopravviva al cambio di ramo, assicuratevi che la key sia identica in entrambi i casi.\n\n## Quando NON servono le key\n\nAggiungere key ovunque \"per sicurezza\" è controproducente: aumenta il rumore e può addirittura impedire il riuso degli Element. Potete tranquillamente farne a meno quando:\n\n- la lista di figli è **statica** (numero e ordine fissi);\n- i figli sono `StatelessWidget` senza stato interno né controller;\n- l'unica cosa che cambia sono i dati mostrati, non la struttura.\n\nUn'eccezione utile: le key nei test e nell'automazione (`find.byKey(const Key('login_button'))`) sono sempre benvenute, perché rendono i test indipendenti dalle stringhe localizzate.\n\n## Checklist pratica\n\n1. La lista può essere **riordinata, filtrata o accorciata**? Metti una `ValueKey` con l'id del modello sulla radice di ogni item.\n2. Gli item hanno stato interno (checkbox, `TextEditingController`, animazioni)? Le key sono obbligatorie.\n3. Vuoi forzare un reset completo? `UniqueKey` o una key che cambia con l'identificatore della risorsa.\n4. Devi conservare lo scroll tra tab? `PageStorageKey`.\n5. Ti serve accedere allo `State` o al `RenderObject`? `GlobalKey`, ma con parsimonia e mai creata in `build`.\n6. Stai usando l'indice come key? Rimuovilo: non risolve nulla.\n\n## Conclusioni\n\nLe Key non sono un dettaglio esotico del framework: sono il meccanismo con cui Flutter decide **cosa riusare e cosa ricreare**. Capire il legame tra widget, Element e `canUpdate` trasforma una serie di bug apparentemente casuali — stato che salta, animazioni che non partono, scroll che si resetta — in problemi con una soluzione deterministica di una riga. La regola d'oro resta semplice: key stabili e derivate dai dati sulla radice dei sottoalberi che si muovono, nient'altro.","https:\u002F\u002Fflutter.it\u002Fstorage\u002Farticles\u002F37230a72-a575-47ac-948e-1930460d996f.jpg","https:\u002F\u002Fimages.unsplash.com\u002Fphoto-1683428506779-9bb1f6ad20ce?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w5NzA2NTJ8MHwxfHJhbmRvbXx8fHx8fHx8fDE3ODgyMzUyODR8&ixlib=rb-4.1.0&q=80&w=1080",{"name":12,"author_url":13,"photo_url":14},"David Birozy","https:\u002F\u002Funsplash.com\u002F@ltdbjd","https:\u002F\u002Funsplash.com\u002Fphotos\u002Fa-close-up-of-a-bunch-of-metal-chains-XqlW1vjbQBQ",null,"published","2026-09-01T04:01:25+00:00","Le Key in Flutter: guida pratica a ValueKey e GlobalKey","Come funzionano le Key in Flutter: ValueKey, ObjectKey, UniqueKey, PageStorageKey e GlobalKey per evitare bug di stato in liste, form e animazioni.",{"id":21,"name":22,"slug":23},1,"Guide","guide",{"id":21,"name":25},"Flutter Bot",[],1789120584556]