[{"data":1,"prerenderedAt":27},["ShallowReactive",2],{"articolo-gestione-dello-stato-in-flutter-senza-pacchetti-valuenotifier-changenotifier-e-listenablebuilder":3,"comments-article-gestione-dello-stato-in-flutter-senza-pacchetti-valuenotifier-changenotifier-e-listenablebuilder":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},138,"Gestione dello stato in Flutter senza pacchetti: ValueNotifier, ChangeNotifier e ListenableBuilder","gestione-dello-stato-in-flutter-senza-pacchetti-valuenotifier-changenotifier-e-listenablebuilder","Prima di aggiungere una dipendenza, Flutter offre già tutto il necessario per gestire lo stato: Listenable, ValueNotifier, ChangeNotifier e ListenableBuilder. Guida pratica con esempi, pattern, test e trappole da evitare.","## Perché partire da ciò che Flutter offre già\n\nQuando si parla di state management in Flutter si pensa subito a Riverpod, BLoC o Signals. Eppure il framework include da sempre un sistema di reattività semplice e collaudato, basato sull'interfaccia `Listenable`: è lo stesso meccanismo che fa funzionare `TextEditingController`, `ScrollController`, `AnimationController` e `PageController`.\n\nPer app di piccole e medie dimensioni, per widget riutilizzabili o per package che non vogliono imporre dipendenze, questa infrastruttura è spesso più che sufficiente. Conoscerla bene aiuta anche a capire come funzionano \"sotto il cofano\" le librerie più note: `ChangeNotifierProvider` di Provider, per esempio, è costruito esattamente su questi mattoni.\n\n## Il contratto: Listenable\n\n`Listenable` è un'interfaccia minimale con due metodi:\n\n```dart\nabstract class Listenable {\n  void addListener(VoidCallback listener);\n  void removeListener(VoidCallback listener);\n}\n```\n\nChi ascolta viene notificato che \"qualcosa è cambiato\", senza ricevere alcun valore. Da qui derivano due specializzazioni fondamentali:\n\n- **`ValueListenable\u003CT>`**: espone anche una proprietà `value` di tipo `T`.\n- **`ChangeNotifier`**: implementazione concreta di `Listenable` che gestisce la lista di listener e offre `notifyListeners()`.\n\n## ValueNotifier: il caso più semplice\n\n`ValueNotifier\u003CT>` è un contenitore di un singolo valore che notifica quando il valore cambia.\n\n```dart\nfinal counter = ValueNotifier\u003Cint>(0);\n\ncounter.value++; \u002F\u002F notifica tutti i listener\n```\n\nNella UI si usa `ValueListenableBuilder`, che ricostruisce **solo** il sottoalbero passato nel builder:\n\n```dart\nclass CounterView extends StatefulWidget {\n  const CounterView({super.key});\n\n  @override\n  State\u003CCounterView> createState() => _CounterViewState();\n}\n\nclass _CounterViewState extends State\u003CCounterView> {\n  final _counter = ValueNotifier\u003Cint>(0);\n\n  @override\n  void dispose() {\n    _counter.dispose(); \u002F\u002F fondamentale\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return Scaffold(\n      body: Center(\n        child: ValueListenableBuilder\u003Cint>(\n          valueListenable: _counter,\n          builder: (context, value, child) {\n            return Column(\n              mainAxisSize: MainAxisSize.min,\n              children: [\n                child!, \u002F\u002F non viene ricostruito\n                Text('$value', style: Theme.of(context).textTheme.displayMedium),\n              ],\n            );\n          },\n          child: const Text('Valore corrente'),\n        ),\n      ),\n      floatingActionButton: FloatingActionButton(\n        onPressed: () => _counter.value++,\n        child: const Icon(Icons.add),\n      ),\n    );\n  }\n}\n```\n\nIl parametro `child` è il dettaglio che molti ignorano: tutto ciò che non dipende dal valore va messo lì, così viene costruito una sola volta e riutilizzato a ogni notifica.\n\n### Attenzione all'uguaglianza\n\n`ValueNotifier` notifica solo se il nuovo valore è **diverso** secondo `==`. Con oggetti mutabili questo è un problema classico:\n\n```dart\nfinal items = ValueNotifier\u003CList\u003CString>>([]);\n\n\u002F\u002F NON notifica: è sempre la stessa istanza di lista\nitems.value.add('nuovo');\n\n\u002F\u002F Corretto: nuova istanza immutabile\nitems.value = [...items.value, 'nuovo'];\n```\n\nLa regola è semplice: usa `ValueNotifier` con tipi immutabili (primitivi, `enum`, classi con `==` basato sui valori, per esempio generate con Freezed o `equatable`).\n\n## ChangeNotifier: stato composto e logica di dominio\n\nQuando lo stato ha più campi e qualche metodo di business, `ChangeNotifier` è la scelta naturale. L'idea è incapsulare i dati e chiamare `notifyListeners()` quando cambiano.\n\n```dart\nclass CartController extends ChangeNotifier {\n  final Map\u003CString, int> _quantities = {};\n\n  Map\u003CString, int> get quantities => Map.unmodifiable(_quantities);\n\n  int get totalItems =>\n      _quantities.values.fold(0, (sum, q) => sum + q);\n\n  bool get isEmpty => _quantities.isEmpty;\n\n  void add(String productId) {\n    _quantities.update(productId, (q) => q + 1, ifAbsent: () => 1);\n    notifyListeners();\n  }\n\n  void remove(String productId) {\n    final current = _quantities[productId];\n    if (current == null) return;\n    if (current \u003C= 1) {\n      _quantities.remove(productId);\n    } else {\n      _quantities[productId] = current - 1;\n    }\n    notifyListeners();\n  }\n\n  void clear() {\n    if (_quantities.isEmpty) return;\n    _quantities.clear();\n    notifyListeners();\n  }\n}\n```\n\nDue accortezze già visibili nell'esempio:\n\n1. **Esporre viste non modificabili** (`Map.unmodifiable`) evita che la UI alteri lo stato aggirando i metodi.\n2. **Non notificare se nulla è cambiato**: ogni `notifyListeners()` superfluo è un rebuild sprecato.\n\n### ListenableBuilder\n\nDa Flutter 3.10 esiste `ListenableBuilder`, la versione generica di `ValueListenableBuilder`: accetta qualsiasi `Listenable` (quindi anche un `ChangeNotifier`) e ricostruisce il builder a ogni notifica.\n\n```dart\nListenableBuilder(\n  listenable: cartController,\n  builder: (context, child) {\n    return Badge(\n      label: Text('${cartController.totalItems}'),\n      child: child,\n    );\n  },\n  child: const Icon(Icons.shopping_cart),\n)\n```\n\nIn passato si usava `AnimatedBuilder` anche per oggetti non animati: funziona ancora (è la stessa implementazione), ma `ListenableBuilder` comunica molto meglio l'intento.\n\n### Combinare più sorgenti con Listenable.merge\n\n```dart\nfinal combined = Listenable.merge([cartController, userController]);\n\nListenableBuilder(\n  listenable: combined,\n  builder: (context, _) => CheckoutSummary(\n    user: userController.user,\n    total: cartController.totalItems,\n  ),\n);\n```\n\nUtile quando una porzione di UI dipende da due controller indipendenti. Ricorda che `Listenable.merge` non va messo dentro `build()` senza cautela: creandolo a ogni rebuild si aggiungono e rimuovono listener continuamente. Meglio costruirlo in `initState` (e non serve farne `dispose`: non possiede i figli).\n\n## Stato asincrono: caricamento, errore, dati\n\nIl pattern si adatta bene anche al caricamento dati, soprattutto combinato con le sealed class di Dart 3.\n\n```dart\nsealed class ArticlesState {\n  const ArticlesState();\n}\n\nclass ArticlesLoading extends ArticlesState {\n  const ArticlesLoading();\n}\n\nclass ArticlesError extends ArticlesState {\n  const ArticlesError(this.message);\n  final String message;\n}\n\nclass ArticlesData extends ArticlesState {\n  const ArticlesData(this.items);\n  final List\u003CArticle> items;\n}\n\nclass ArticlesController extends ValueNotifier\u003CArticlesState> {\n  ArticlesController(this._repository) : super(const ArticlesLoading());\n\n  final ArticleRepository _repository;\n  bool _disposed = false;\n\n  Future\u003Cvoid> load() async {\n    value = const ArticlesLoading();\n    try {\n      final items = await _repository.fetchAll();\n      if (_disposed) return;\n      value = ArticlesData(items);\n    } catch (e) {\n      if (_disposed) return;\n      value = ArticlesError(e.toString());\n    }\n  }\n\n  @override\n  void dispose() {\n    _disposed = true;\n    super.dispose();\n  }\n}\n```\n\nNella UI, lo `switch` esaustivo sui pattern rende impossibile dimenticare un caso:\n\n```dart\nValueListenableBuilder\u003CArticlesState>(\n  valueListenable: controller,\n  builder: (context, state, _) => switch (state) {\n    ArticlesLoading() => const Center(child: CircularProgressIndicator()),\n    ArticlesError(:final message) => ErrorView(message: message),\n    ArticlesData(:final items) => ArticleList(items: items),\n  },\n)\n```\n\nNota il flag `_disposed`: se l'utente lascia la schermata mentre la richiesta è in volo, assegnare `value` dopo il `dispose()` lancia un'eccezione (`A ValueNotifier\u003C...> was used after being disposed`).\n\n## Propagare il controller nell'albero\n\nSenza pacchetti, la via idiomatica è `InheritedNotifier`, che unisce la propagazione di `InheritedWidget` all'ascolto automatico di un `Listenable`:\n\n```dart\nclass CartScope extends InheritedNotifier\u003CCartController> {\n  const CartScope({\n    super.key,\n    required CartController controller,\n    required super.child,\n  }) : super(notifier: controller);\n\n  static CartController of(BuildContext context) {\n    final scope = context.dependOnInheritedWidgetOfExactType\u003CCartScope>();\n    assert(scope != null, 'CartScope non trovato nell\\'albero');\n    return scope!.notifier!;\n  }\n}\n```\n\nI widget che usano `CartScope.of(context)` si ricostruiscono automaticamente a ogni `notifyListeners()`. Per un controllo più fine sui rebuild, si può esporre il controller senza dipendenza (con `getElementForInheritedWidgetOfExactType`) e usare `ListenableBuilder` solo dove serve.\n\n## Rebuild chirurgici\n\nIl punto di forza di questo approccio è che **non serve `setState` sull'intera schermata**: il builder ricostruisce solo il sottoalbero interessato. Per ridurre ulteriormente i rebuild quando un `ChangeNotifier` ha molti campi, si possono esporre più `ValueNotifier` granulari:\n\n```dart\nclass FilterController {\n  final query = ValueNotifier\u003CString>('');\n  final onlyFavorites = ValueNotifier\u003Cbool>(false);\n  final sort = ValueNotifier\u003CSortOrder>(SortOrder.recent);\n\n  void dispose() {\n    query.dispose();\n    onlyFavorites.dispose();\n    sort.dispose();\n  }\n}\n```\n\nCosì la checkbox dei preferiti non si ricostruisce quando cambia il testo di ricerca. È lo stesso principio del \"select\" delle librerie di state management, ottenuto a mano.\n\nUn'alternativa elegante è esporre pubblicamente solo il tipo `ValueListenable\u003CT>`, mantenendo privato il notifier scrivibile:\n\n```dart\nclass SessionController {\n  final _user = ValueNotifier\u003CUser?>(null);\n  ValueListenable\u003CUser?> get user => _user; \u002F\u002F sola lettura per la UI\n}\n```\n\n## Testare i notifier\n\nUn vantaggio enorme: sono semplici oggetti Dart, testabili senza `WidgetTester`.\n\n```dart\ntest('add incrementa la quantità e notifica', () {\n  final cart = CartController();\n  var notifications = 0;\n  cart.addListener(() => notifications++);\n\n  cart.add('p1');\n  cart.add('p1');\n\n  expect(cart.totalItems, 2);\n  expect(notifications, 2);\n});\n```\n\nPer i widget test, `pumpWidget` più `tester.pump()` dopo l'azione è sufficiente, perché le notifiche sono sincrone.\n\n## Errori comuni da evitare\n\n- **Dimenticare `dispose()`**: ogni notifier creato in uno `State` va distrutto, altrimenti i listener restano agganciati e si creano memory leak. In debug Flutter segnala l'uso dopo il dispose, ma non il leak.\n- **Chiamare `notifyListeners()` durante il build**: provoca l'eccezione \"setState() or markNeedsBuild() called during build\". Se è inevitabile, rimanda con `WidgetsBinding.instance.addPostFrameCallback`.\n- **Notificare troppo**: un `notifyListeners()` per ogni campo modificato in un ciclo genera N rebuild. Raggruppa le modifiche e notifica una volta sola.\n- **Stato mutabile condiviso**: esporre liste modificabili rende imprevedibili sia la UI sia i test.\n- **Usare `ChangeNotifier` come singleton globale senza ciclo di vita**: va bene per sessioni e configurazioni, ma per lo stato di una schermata il controller deve nascere e morire con essa.\n\n## Quando questo approccio basta (e quando no)\n\nÈ più che sufficiente quando:\n\n- lo stato è locale a una schermata o a un flusso;\n- stai scrivendo un package e vuoi zero dipendenze;\n- il team è piccolo e preferisci API del framework a convenzioni esterne.\n\nConviene invece valutare soluzioni più strutturate quando servono dependency injection con scope complessi, invalidazione automatica e caching delle richieste, composizione di molti stati derivati, o strumenti di debug e time-travel.\n\nLa buona notizia è che la migrazione è quasi indolore: un `ChangeNotifier` ben scritto può essere avvolto in un `ChangeNotifierProvider`, esposto da un provider Riverpod o sostituito da un Cubit con modifiche minime, perché la logica di dominio resta separata dalla UI. Partire da `ValueNotifier` e `ListenableBuilder` è quindi una scelta pragmatica: meno dipendenze oggi, nessun vincolo domani.","https:\u002F\u002Fflutter.it\u002Fstorage\u002Farticles\u002F10ed85c5-36b0-4b09-935b-fa35e3e86b20.jpg","https:\u002F\u002Fimages.unsplash.com\u002Fphoto-1610986602726-19f650133f7a?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w5NzA2NTJ8MHwxfHJhbmRvbXx8fHx8fHx8fDE3OTI5MDA4OTB8&ixlib=rb-4.1.0&q=80&w=1080",{"name":12,"author_url":13,"photo_url":14},"Ferenc Almasi","https:\u002F\u002Funsplash.com\u002F@flowforfrank","https:\u002F\u002Funsplash.com\u002Fphotos\u002Ftext-cWUPyE8qggw",null,"published","2026-10-25T04:01:30+00:00","ValueNotifier e ListenableBuilder: stato senza pacchetti","Come gestire lo stato in Flutter con ValueNotifier, ChangeNotifier e ListenableBuilder: esempi pratici, rebuild mirati, test e trappole da evitare.",{"id":21,"name":22,"slug":23},1,"Guide","guide",{"id":21,"name":25},"Flutter Bot",[],1793098103174]