[{"data":1,"prerenderedAt":27},["ShallowReactive",2],{"articolo-signals-in-flutter-reattivita-fine-grained-con-il-pacchetto-signals":3,"comments-article-signals-in-flutter-reattivita-fine-grained-con-il-pacchetto-signals":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},123,"Signals in Flutter: reattività fine-grained con il pacchetto signals","signals-in-flutter-reattivita-fine-grained-con-il-pacchetto-signals","I signals portano in Flutter la reattività fine-grained: stato osservabile, valori derivati automatici e rebuild mirati solo dei widget interessati. Guida pratica a signal, computed, effect, Watch e gestione dell'async.","## Cosa sono i signals\n\nI *signals* sono un modello di reattività nato nel mondo JavaScript (SolidJS, Angular, Preact, Vue) e arrivato in Dart grazie al pacchetto [`signals`](https:\u002F\u002Fpub.dev\u002Fpackages\u002Fsignals). L'idea è semplice: un signal è un contenitore di valore che **tiene traccia automaticamente di chi lo legge**. Quando il valore cambia, solo chi lo ha letto viene notificato.\n\nLa differenza rispetto a `ChangeNotifier` o ai provider classici è il livello di granularità. Con un `ChangeNotifier` sei tu a decidere quando chiamare `notifyListeners()` e quali widget ascoltare; con i signals il grafo delle dipendenze viene costruito **a runtime, durante la lettura**, e gli aggiornamenti si propagano solo lungo i rami effettivamente interessati. È la cosiddetta *fine-grained reactivity*.\n\nIl risultato pratico in Flutter è notevole: niente `select` manuali, niente `Consumer` annidati per limitare i rebuild, nessuna dipendenza dal `BuildContext` per leggere lo stato.\n\n## Installazione\n\n```yaml\ndependencies:\n  signals: ^6.0.0\n```\n\nIl pacchetto `signals` include sia il core Dart puro (`signals_core`) sia le estensioni Flutter (`signals_flutter`). Se stai scrivendo un package di dominio senza dipendenze da Flutter, puoi usare direttamente `signals_core`.\n\n## Le tre primitive: signal, computed, effect\n\n### signal\n\nUn signal è uno stato mutabile osservabile:\n\n```dart\nimport 'package:signals\u002Fsignals.dart';\n\nfinal counter = signal(0);\n\nvoid increment() => counter.value++;\n```\n\nLa lettura avviene con `.value`, la scrittura assegnando a `.value`. Esiste anche `counter.set(5)` e, per i casi in cui vuoi forzare la notifica anche con valore identico, `counter.set(5, force: true)`.\n\n### computed\n\nUn `computed` è un valore **derivato** e memoizzato: si ricalcola solo quando una delle sue dipendenze cambia, ed è *lazy* (non viene calcolato finché qualcuno non lo legge).\n\n```dart\nfinal items = signal(\u003CCartItem>[]);\nfinal coupon = signal\u003Cdouble>(0);\n\nfinal subtotal = computed(\n  () => items.value.fold\u003Cdouble>(0, (sum, i) => sum + i.price * i.qty),\n);\n\nfinal total = computed(() => subtotal.value * (1 - coupon.value));\n```\n\nNota che `total` dipende da `subtotal`, che a sua volta dipende da `items`: il grafo si costruisce da solo. Se modifichi solo `coupon`, `subtotal` **non** viene ricalcolato.\n\n### effect\n\nUn `effect` esegue codice imperativo ogni volta che una delle dipendenze lette al suo interno cambia. Restituisce una funzione di dispose.\n\n```dart\nfinal dispose = effect(() {\n  debugPrint('Totale aggiornato: ${total.value}');\n});\n\n\u002F\u002F quando non serve più\ndispose();\n```\n\nIl callback può restituire a sua volta una funzione di cleanup, eseguita prima di ogni ri-esecuzione e al dispose:\n\n```dart\neffect(() {\n  final sub = repository.watchUser(userId.value).listen(_onUser);\n  return sub.cancel; \u002F\u002F cleanup\n});\n```\n\nRegola d'oro: **i computed sono puri, gli effect hanno side effect**. Non modificare signals dentro un `computed`.\n\n## Collegare i signals ai widget\n\nCi sono due modi principali, entrambi forniti da `signals_flutter`.\n\n### 1. Il widget `Watch`\n\n`Watch` ricostruisce **solo il suo subtree** quando cambia un signal letto nel builder:\n\n```dart\nclass CartTotal extends StatelessWidget {\n  const CartTotal({super.key});\n\n  @override\n  Widget build(BuildContext context) {\n    return Column(\n      children: [\n        const Text('Totale carrello'), \u002F\u002F non viene mai ricostruito\n        Watch((context) => Text('€ ${total.value.toStringAsFixed(2)}')),\n      ],\n    );\n  }\n}\n```\n\nQuesto è l'approccio consigliato: isolare le parti dinamiche riduce al minimo il lavoro del framework, esattamente come faresti con `ValueListenableBuilder` ma senza doverne gestire l'istanza.\n\n### 2. L'extension `.watch(context)`\n\nSe preferisci una sintassi più compatta, puoi leggere il signal direttamente nel `build` di un widget:\n\n```dart\nclass CounterText extends StatelessWidget {\n  const CounterText({super.key});\n\n  @override\n  Widget build(BuildContext context) {\n    final value = counter.watch(context);\n    return Text('$value');\n  }\n}\n```\n\nIn questo caso viene ricostruito l'intero widget, quindi mantienilo piccolo. Esiste anche `.listen(context, callback)` per reagire ai cambiamenti senza ricostruire (utile per snackbar, navigazione, dialog).\n\n## Signals locali e ciclo di vita\n\nI signals globali vanno bene per stato applicativo condiviso, ma spesso serve stato **locale a una schermata**. Il pacchetto offre `SignalsMixin` da usare su uno `State`: i signals creati con i metodi `create*` vengono automaticamente osservati e distrutti con il widget.\n\n```dart\nclass SearchPage extends StatefulWidget {\n  const SearchPage({super.key});\n\n  @override\n  State\u003CSearchPage> createState() => _SearchPageState();\n}\n\nclass _SearchPageState extends State\u003CSearchPage> with SignalsMixin {\n  late final query = createSignal('');\n  late final isValid = createComputed(() => query.value.trim().length >= 3);\n\n  @override\n  void initState() {\n    super.initState();\n    createEffect(() {\n      if (isValid.value) {\n        debugPrint('Ricerca: ${query.value}');\n      }\n    });\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return Column(\n      children: [\n        TextField(onChanged: (v) => query.value = v),\n        ElevatedButton(\n          onPressed: isValid.value ? _search : null,\n          child: const Text('Cerca'),\n        ),\n      ],\n    );\n  }\n\n  void _search() {\u002F* ... *\u002F}\n}\n```\n\nCon `SignalsMixin` la lettura di `isValid.value` dentro `build` è già tracciata: non serve `Watch`. Per i signals globali che vuoi far morire quando nessuno li ascolta più puoi usare `signal(0, autoDispose: true)`.\n\n## Gestire l'asincrono\n\nIl punto di forza dei signals emerge con i dati remoti. `futureSignal` e `streamSignal` incapsulano uno stato asincrono in un `AsyncSignal` che espone `AsyncState` (loading \u002F data \u002F error):\n\n```dart\nfinal userId = signal('u-42');\n\nfinal userSignal = computedAsync(() async {\n  final id = userId.value; \u002F\u002F dipendenza tracciata\n  return api.fetchUser(id);\n});\n```\n\nQuando `userId` cambia, la richiesta viene rifatta automaticamente e il signal passa in stato di loading. Nella UI:\n\n```dart\nWatch((context) {\n  return userSignal.value.map(\n    data: (user) => UserCard(user: user),\n    error: (err, _) => ErrorView(message: '$err'),\n    loading: () => const CircularProgressIndicator(),\n  );\n});\n```\n\nPer i flussi continui (WebSocket, Firestore, sensori) c'è `streamSignal`, mentre `toSignal()` converte un `Future`\u002F`Stream` esistente. Il vantaggio rispetto a `FutureBuilder` è che lo stato vive fuori dal widget tree: sopravvive ai rebuild e può essere condiviso tra più schermate.\n\n## batch e untracked\n\nSe modifichi più signals in sequenza, ogni assegnazione innesca una propagazione. `batch` raggruppa le modifiche in una sola notifica:\n\n```dart\nbatch(() {\n  firstName.value = 'Mario';\n  lastName.value = 'Rossi';\n  age.value = 42;\n});\n```\n\n`untracked` invece legge un valore **senza** creare una dipendenza, utile dentro un effect quando ti serve il dato ma non vuoi ri-eseguire al suo cambiamento:\n\n```dart\neffect(() {\n  final q = query.value; \u002F\u002F dipendenza\n  final page = untracked(() => currentPage.value); \u002F\u002F non dipendenza\n  analytics.log('search', {'q': q, 'page': page});\n});\n```\n\n## Un pattern architetturale pulito\n\nI signals si sposano bene con un'architettura a livelli: il repository resta Dart puro, il view model espone signals, la UI osserva.\n\n```dart\nclass TodoViewModel {\n  TodoViewModel(this._repo);\n\n  final TodoRepository _repo;\n\n  final filter = signal(TodoFilter.all);\n  late final todos = computedAsync(() => _repo.fetch());\n\n  late final visible = computed(() {\n    final list = todos.value.value ?? const \u003CTodo>[];\n    return switch (filter.value) {\n      TodoFilter.all => list,\n      TodoFilter.done => list.where((t) => t.done).toList(),\n      TodoFilter.open => list.where((t) => !t.done).toList(),\n    };\n  });\n\n  Future\u003Cvoid> toggle(Todo todo) async {\n    await _repo.toggle(todo.id);\n    todos.refresh();\n  }\n}\n```\n\nIl view model non conosce il `BuildContext` ed è testabile con semplici unit test: crei l'istanza, cambi un signal, leggi il `computed`.\n\n```dart\ntest('filtra i todo completati', () {\n  final vm = TodoViewModel(FakeRepo());\n  vm.filter.value = TodoFilter.done;\n  expect(vm.visible.value.every((t) => t.done), isTrue);\n});\n```\n\n## Quando conviene (e quando no)\n\n**Conviene** se:\n\n- hai molti valori derivati l'uno dall'altro e vuoi evitare ricalcoli manuali;\n- vuoi rebuild chirurgici in liste o dashboard con molti elementi indipendenti;\n- vuoi logica di stato completamente disaccoppiata da Flutter.\n\n**Pensaci due volte** se:\n\n- il team è già produttivo con BLoC o Riverpod: il guadagno non giustifica una migrazione totale;\n- ti serve un ecosistema molto maturo di tooling e documentazione in italiano: i signals sono relativamente recenti nel mondo Dart;\n- ami il flusso di eventi esplicito e tracciabile di BLoC: i signals sono più \"magici\" e il grafo di dipendenze è implicito.\n\nNulla vieta un'adozione **incrementale**: i signals convivono senza problemi con Riverpod o BLoC, e puoi iniziare da una singola schermata complessa.\n\n## Errori comuni da evitare\n\n1. **Leggere `.value` fuori da un contesto tracciato** e aspettarsi un rebuild: se non usi `Watch`, `.watch(context)` o `SignalsMixin`, nessuno aggiorna la UI.\n2. **Mutare una lista in-place** (`items.value.add(x)`): i signals confrontano per identità, quindi serve assegnare una nuova lista, oppure usare `listSignal`\u002F`mapSignal` che gestiscono le mutazioni.\n3. **Modificare signals dentro un `computed`**: genera cicli e comportamenti imprevedibili.\n4. **Dimenticare il dispose degli effect globali**: usa `SignalsMixin` o conserva la funzione restituita da `effect`.\n5. **Avvolgere mezzo albero in un `Watch`**: perdi il vantaggio principale, cioè i rebuild mirati.\n\n## Conclusione\n\nI signals offrono un modello mentale compatto — stato, derivazione, effetto — con prestazioni ottime grazie al tracking automatico delle dipendenze. In Flutter si traducono in meno boilerplate rispetto ai `ChangeNotifier`, rebuild più precisi e una logica di dominio che resta Dart puro. Se stai avviando un progetto nuovo o devi rifattorizzare una schermata con molti stati interdipendenti, vale la pena provarli: il salto concettuale è minimo, il guadagno in leggibilità spesso notevole.","https:\u002F\u002Fflutter.it\u002Fstorage\u002Farticles\u002F26bd437a-0a35-46c1-89f4-d7f9c48d93c0.jpg","https:\u002F\u002Fimages.unsplash.com\u002Fphoto-1790746517103-941f8a414461?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w5NzA2NTJ8MHwxfHJhbmRvbXx8fHx8fHx8fDE3OTE2MDQ4OTd8&ixlib=rb-4.1.0&q=80&w=1080",{"name":12,"author_url":13,"photo_url":14},"Brecht Corbeel","https:\u002F\u002Funsplash.com\u002F@brechtcorbeel","https:\u002F\u002Funsplash.com\u002Fphotos\u002Fwave-word-formed-by-cubes-PO3J29iOzZ8",null,"published","2026-10-10T04:01:38+00:00","Signals in Flutter: reattività fine-grained","Guida pratica ai signals in Flutter: signal, computed, effect, widget Watch, SignalsMixin e gestione dell'async per rebuild mirati e codice più pulito.",{"id":21,"name":22,"slug":23},1,"Guide","guide",{"id":21,"name":25},"Flutter Bot",[],1792316902631]