[{"data":1,"prerenderedAt":78},["ShallowReactive",2],{"tutorial-stato-reattivo-senza-package-in-flutter-con-valuenotifier-e-valuelistenablebuilder":3,"comments-tutorial-stato-reattivo-senza-package-in-flutter-con-valuenotifier-e-valuelistenablebuilder":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},134,"Stato reattivo senza package in Flutter con ValueNotifier e ValueListenableBuilder","stato-reattivo-senza-package-in-flutter-con-valuenotifier-e-valuelistenablebuilder","Impara a gestire lo stato in modo reattivo usando solo gli strumenti inclusi in Flutter: ValueNotifier e ValueListenableBuilder, per ricostruire solo i widget che servono davvero.","Quando si inizia con Flutter il primo strumento che si impara per aggiornare l'interfaccia è `setState`. Funziona, ma ha un limite: ricostruisce **l'intero widget** che lo contiene, anche le parti che non sono cambiate.\n\nPrima di installare Provider, Riverpod o BLoC, vale la pena conoscere una coppia di classi che Flutter mette a disposizione *out of the box*:\n\n- **`ValueNotifier\u003CT>`**: un contenitore di un valore che avvisa i suoi ascoltatori quando il valore cambia;\n- **`ValueListenableBuilder\u003CT>`**: un widget che ascolta un `ValueNotifier` e ricostruisce **solo il suo sottoalbero**.\n\nSono leggerissimi, non richiedono dipendenze esterne e sono perfetti per lo stato locale di una schermata: un contatore, un interruttore, una lista di elementi, lo stato di caricamento di un pulsante.\n\nIn questo tutorial costruiremo passo passo una piccola app \"Lista della spesa\" che usa solo `ValueNotifier`, imparando anche gli errori classici (dimenticare `dispose`, mutare una lista senza notificare) e come ottimizzare i rebuild.\n\n> Prerequisiti: Flutter installato, conoscenza di base di `StatefulWidget` e `setState`.","https:\u002F\u002Fflutter.it\u002Fstorage\u002Ftutorials\u002F67388938-6fa0-4b1f-9be2-847b194c44da.jpg","https:\u002F\u002Fimages.unsplash.com\u002Fphoto-1644329843283-640d00509d43?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w5NzA2NTJ8MHwxfHJhbmRvbXx8fHx8fHx8fDE3OTM2ODAyODh8&ixlib=rb-4.1.0&q=80&w=1080",{"name":12,"author_url":13,"photo_url":14},"Annie Spratt","https:\u002F\u002Funsplash.com\u002F@anniespratt","https:\u002F\u002Funsplash.com\u002Fphotos\u002Fa-piece-of-paper-with-a-note-attached-to-it-3sgir4ErK5U","https:\u002F\u002Fwww.youtube.com\u002Fwatch?v=JOCFb6Hripo",{"youtube_id":17,"duration_seconds":18,"published_at":19},"JOCFb6Hripo",121,"2026-11-03T12:29:18+00:00","beginner",30,"3.x","published","2026-11-03T04:31:28+00:00","ValueNotifier e ValueListenableBuilder in Flutter","Guida beginner a ValueNotifier e ValueListenableBuilder in Flutter: stato reattivo senza package, rebuild mirati, dispose e gestione di liste.",{"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},896,"Creare il progetto e capire il limite di setState","Crea un nuovo progetto e sostituisci il contenuto di `lib\u002Fmain.dart` con una schermata minimale.\n\nNell'esempio qui sotto usiamo `setState` in modo classico: ogni tocco sul pulsante ricostruisce **tutto** il `build` della schermata, compreso il testo \"Intestazione statica\" che non cambia mai.\n\nAggiungi la `print` dentro `build`: quando premi il pulsante vedrai nel terminale che il metodo viene eseguito ogni volta. Con pochi widget non è un problema, ma in una schermata complessa è lavoro sprecato.\n\nTieni a mente questa riga di log: nei prossimi passi la faremo sparire.","import 'package:flutter\u002Fmaterial.dart';\n\nvoid main() => runApp(const MyApp());\n\nclass MyApp extends StatelessWidget {\n  const MyApp({super.key});\n\n  @override\n  Widget build(BuildContext context) {\n    return MaterialApp(\n      title: 'Demo ValueNotifier',\n      theme: ThemeData(colorSchemeSeed: Colors.teal, useMaterial3: true),\n      home: const ContatoreSetState(),\n    );\n  }\n}\n\nclass ContatoreSetState extends StatefulWidget {\n  const ContatoreSetState({super.key});\n\n  @override\n  State\u003CContatoreSetState> createState() => _ContatoreSetStateState();\n}\n\nclass _ContatoreSetStateState extends State\u003CContatoreSetState> {\n  int _contatore = 0;\n\n  @override\n  Widget build(BuildContext context) {\n    debugPrint('build() della schermata eseguito');\n    return Scaffold(\n      appBar: AppBar(title: const Text('setState')),\n      body: Center(\n        child: Column(\n          mainAxisAlignment: MainAxisAlignment.center,\n          children: [\n            const Text('Intestazione statica'),\n            Text('$_contatore', style: const TextStyle(fontSize: 48)),\n          ],\n        ),\n      ),\n      floatingActionButton: FloatingActionButton(\n        onPressed: () => setState(() => _contatore++),\n        child: const Icon(Icons.add),\n      ),\n    );\n  }\n}","dart","L'app mostra un contatore. Nella console di debug compare \"build() della schermata eseguito\" a ogni tocco del pulsante.",null,{"id":43,"position":44,"title":45,"body":46,"code_snippet":47,"code_language":39,"expected_result":48,"demo_url":41,"video_url":41},897,2,"Il primo ValueNotifier con ValueListenableBuilder","Un `ValueNotifier\u003CT>` è un oggetto che contiene un valore (`.value`) e notifica chi lo ascolta ogni volta che il valore viene **sostituito**.\n\n`ValueListenableBuilder\u003CT>` si occupa di iscriversi e disiscriversi automaticamente: riceve il notifier in `valueListenable` e ricostruisce solo la funzione `builder`.\n\nNota tre dettagli importanti:\n\n1. il `ValueNotifier` è dichiarato come campo `final` dello `State`, **non** dentro `build` (altrimenti verrebbe ricreato a ogni ricostruzione);\n2. per aggiornare il valore si scrive `_contatore.value++` oppure `_contatore.value = nuovoValore`: niente `setState`;\n3. il `build` della schermata ora viene eseguito **una sola volta**, mentre il solo `Text` del contatore si aggiorna.","class ContatoreNotifier extends StatefulWidget {\n  const ContatoreNotifier({super.key});\n\n  @override\n  State\u003CContatoreNotifier> createState() => _ContatoreNotifierState();\n}\n\nclass _ContatoreNotifierState extends State\u003CContatoreNotifier> {\n  final ValueNotifier\u003Cint> _contatore = ValueNotifier\u003Cint>(0);\n\n  @override\n  Widget build(BuildContext context) {\n    debugPrint('build() della schermata eseguito');\n    return Scaffold(\n      appBar: AppBar(title: const Text('ValueNotifier')),\n      body: Center(\n        child: Column(\n          mainAxisAlignment: MainAxisAlignment.center,\n          children: [\n            const Text('Intestazione statica'),\n            ValueListenableBuilder\u003Cint>(\n              valueListenable: _contatore,\n              builder: (context, valore, _) {\n                debugPrint('rebuild del solo contatore');\n                return Text('$valore', style: const TextStyle(fontSize: 48));\n              },\n            ),\n          ],\n        ),\n      ),\n      floatingActionButton: FloatingActionButton(\n        onPressed: () => _contatore.value++,\n        child: const Icon(Icons.add),\n      ),\n    );\n  }\n}","Il contatore si incrementa, ma in console compare solo \"rebuild del solo contatore\": il build della schermata non viene più rieseguito.",{"id":50,"position":51,"title":52,"body":53,"code_snippet":54,"code_language":39,"expected_result":55,"demo_url":41,"video_url":41},898,3,"Liberare le risorse con dispose (e il parametro child)","Un `ValueNotifier` è un oggetto ascoltabile: se non viene chiuso quando la schermata viene rimossa, i listener restano appesi e si rischia un memory leak. La regola è semplice: **ogni notifier creato dallo State va chiuso in `dispose()`**.\n\nIn più `ValueListenableBuilder` offre il parametro `child`: serve per i widget che **non dipendono** dal valore. Il `child` viene costruito una sola volta e passato al builder come terzo argomento, evitando di ricrearlo a ogni notifica. È un'ottimizzazione gratuita, utile quando il sottoalbero contiene widget costosi (immagini, icone grandi, liste).","class _ContatoreNotifierState extends State\u003CContatoreNotifier> {\n  final ValueNotifier\u003Cint> _contatore = ValueNotifier\u003Cint>(0);\n\n  @override\n  void dispose() {\n    _contatore.dispose(); \u002F\u002F sempre prima di super.dispose()\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return Scaffold(\n      appBar: AppBar(title: const Text('ValueNotifier')),\n      body: Center(\n        child: ValueListenableBuilder\u003Cint>(\n          valueListenable: _contatore,\n          \u002F\u002F Costruito una sola volta: non dipende dal valore.\n          child: const Icon(Icons.shopping_cart, size: 64),\n          builder: (context, valore, child) {\n            return Column(\n              mainAxisSize: MainAxisSize.min,\n              children: [\n                child!,\n                Text('Articoli: $valore',\n                    style: Theme.of(context).textTheme.headlineMedium),\n              ],\n            );\n          },\n        ),\n      ),\n      floatingActionButton: FloatingActionButton(\n        onPressed: () => _contatore.value++,\n        child: const Icon(Icons.add),\n      ),\n    );\n  }\n}","L'app funziona come prima, l'icona non viene ricreata a ogni aggiornamento e chiudendo la schermata non restano listener attivi.",{"id":57,"position":58,"title":59,"body":60,"code_snippet":61,"code_language":39,"expected_result":62,"demo_url":41,"video_url":41},899,4,"Gestire una lista: l'insidia dell'uguaglianza","Qui si nasconde l'errore più comune dei principianti. `ValueNotifier` notifica solo se il **nuovo valore è diverso dal precedente** (confronto con `==`). Per gli oggetti come `List`, `==` confronta l'identità: se aggiungi un elemento alla stessa lista con `add()`, l'oggetto resta lo stesso e **nessuna notifica viene inviata**.\n\nLa soluzione corretta è creare una **nuova lista** ogni volta:\n\n```dart\n_articoli.value = [..._articoli.value, nuovo]; \u002F\u002F ✅ notifica\n_articoli.value.add(nuovo);                    \u002F\u002F ❌ non notifica\n```\n\nCostruiamo quindi la lista della spesa: un `TextField` per inserire l'articolo e una `ListView` che reagisce alle modifiche.","class ListaSpesa extends StatefulWidget {\n  const ListaSpesa({super.key});\n\n  @override\n  State\u003CListaSpesa> createState() => _ListaSpesaState();\n}\n\nclass _ListaSpesaState extends State\u003CListaSpesa> {\n  final ValueNotifier\u003CList\u003CString>> _articoli = ValueNotifier\u003CList\u003CString>>([]);\n  final TextEditingController _campo = TextEditingController();\n\n  void _aggiungi() {\n    final testo = _campo.text.trim();\n    if (testo.isEmpty) return;\n    \u002F\u002F Nuova lista -> il notifier avvisa gli ascoltatori\n    _articoli.value = [..._articoli.value, testo];\n    _campo.clear();\n  }\n\n  void _rimuovi(int indice) {\n    final copia = [..._articoli.value]..removeAt(indice);\n    _articoli.value = copia;\n  }\n\n  @override\n  void dispose() {\n    _articoli.dispose();\n    _campo.dispose();\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return Scaffold(\n      appBar: AppBar(title: const Text('Lista della spesa')),\n      body: Column(\n        children: [\n          Padding(\n            padding: const EdgeInsets.all(16),\n            child: Row(\n              children: [\n                Expanded(\n                  child: TextField(\n                    controller: _campo,\n                    decoration: const InputDecoration(\n                      labelText: 'Nuovo articolo',\n                      border: OutlineInputBorder(),\n                    ),\n                    onSubmitted: (_) => _aggiungi(),\n                  ),\n                ),\n                const SizedBox(width: 8),\n                IconButton.filled(\n                  onPressed: _aggiungi,\n                  icon: const Icon(Icons.add),\n                ),\n              ],\n            ),\n          ),\n          Expanded(\n            child: ValueListenableBuilder\u003CList\u003CString>>(\n              valueListenable: _articoli,\n              builder: (context, lista, _) {\n                if (lista.isEmpty) {\n                  return const Center(child: Text('Nessun articolo'));\n                }\n                return ListView.builder(\n                  itemCount: lista.length,\n                  itemBuilder: (context, i) => ListTile(\n                    leading: CircleAvatar(child: Text('${i + 1}')),\n                    title: Text(lista[i]),\n                    trailing: IconButton(\n                      icon: const Icon(Icons.delete_outline),\n                      onPressed: () => _rimuovi(i),\n                    ),\n                  ),\n                );\n              },\n            ),\n          ),\n        ],\n      ),\n    );\n  }\n}","Scrivendo un articolo e premendo + l'elemento compare nella lista; il cestino lo rimuove. Solo la ListView viene ricostruita.",{"id":64,"position":65,"title":66,"body":67,"code_snippet":68,"code_language":39,"expected_result":69,"demo_url":41,"video_url":41},900,5,"Ascoltare il TextEditingController (è anche lui un ValueNotifier)","Una scoperta utile: molte classi di Flutter **estendono** `ValueNotifier`. `TextEditingController`, per esempio, è un `ValueNotifier\u003CTextEditingValue>`, quindi puoi passarlo direttamente a un `ValueListenableBuilder`.\n\nSfruttiamolo per abilitare il pulsante \"Aggiungi\" solo quando il campo non è vuoto, senza `setState` e senza listener manuali: il resto della schermata non si ricostruisce mentre l'utente digita.\n\nSostituisci il `Row` del passo precedente con questo blocco.","Row(\n  children: [\n    Expanded(\n      child: TextField(\n        controller: _campo,\n        decoration: const InputDecoration(\n          labelText: 'Nuovo articolo',\n          border: OutlineInputBorder(),\n        ),\n        onSubmitted: (_) => _aggiungi(),\n      ),\n    ),\n    const SizedBox(width: 8),\n    ValueListenableBuilder\u003CTextEditingValue>(\n      valueListenable: _campo, \u002F\u002F TextEditingController è un ValueNotifier!\n      builder: (context, value, _) {\n        final abilitato = value.text.trim().isNotEmpty;\n        return FilledButton.icon(\n          onPressed: abilitato ? _aggiungi : null,\n          icon: const Icon(Icons.add),\n          label: const Text('Aggiungi'),\n        );\n      },\n    ),\n  ],\n)","Il pulsante \"Aggiungi\" resta disattivato finché il campo è vuoto e si attiva appena si digita qualcosa.",{"id":71,"position":72,"title":73,"body":74,"code_snippet":75,"code_language":39,"expected_result":76,"demo_url":41,"video_url":41},901,6,"Estrarre un controller riutilizzabile e combinare più notifier","Finché i notifier vivono nello `State` va bene, ma se vuoi testare la logica o condividerla conviene spostarla in una classe separata, una sorta di mini view-model senza dipendenze esterne.\n\nNell'esempio il `SpesaController` espone due notifier: la lista e un filtro \"solo da comprare\". Nidificando due `ValueListenableBuilder` ottieni una UI che reagisce a entrambi.\n\n> Suggerimento: se hai bisogno di combinare molti valori, valuta `Listenable.merge([a, b])` con un `ListenableBuilder`, oppure passa a un package di state management quando la logica cresce. `ValueNotifier` resta la scelta ideale per lo stato semplice e locale.","class Articolo {\n  const Articolo({required this.nome, this.comprato = false});\n  final String nome;\n  final bool comprato;\n\n  Articolo copyWith({bool? comprato}) =>\n      Articolo(nome: nome, comprato: comprato ?? this.comprato);\n}\n\nclass SpesaController {\n  final ValueNotifier\u003CList\u003CArticolo>> articoli = ValueNotifier([]);\n  final ValueNotifier\u003Cbool> soloDaComprare = ValueNotifier(false);\n\n  void aggiungi(String nome) {\n    articoli.value = [...articoli.value, Articolo(nome: nome)];\n  }\n\n  void togglaComprato(int indice) {\n    final copia = [...articoli.value];\n    copia[indice] = copia[indice].copyWith(comprato: !copia[indice].comprato);\n    articoli.value = copia;\n  }\n\n  void dispose() {\n    articoli.dispose();\n    soloDaComprare.dispose();\n  }\n}\n\n\u002F\u002F Uso nella UI\nValueListenableBuilder\u003Cbool>(\n  valueListenable: controller.soloDaComprare,\n  builder: (context, filtro, _) {\n    return ValueListenableBuilder\u003CList\u003CArticolo>>(\n      valueListenable: controller.articoli,\n      builder: (context, lista, _) {\n        final visibili =\n            filtro ? lista.where((a) => !a.comprato).toList() : lista;\n        return ListView.builder(\n          itemCount: visibili.length,\n          itemBuilder: (context, i) => CheckboxListTile(\n            value: visibili[i].comprato,\n            title: Text(visibili[i].nome),\n            onChanged: (_) => controller.togglaComprato(lista.indexOf(visibili[i])),\n          ),\n        );\n      },\n    );\n  },\n)","La lista mostra le checkbox, il filtro nasconde gli articoli già comprati e la logica è isolata in una classe testabile senza dipendere da Flutter widget.",[],1793710661625]