Skip to Content
FlutterLezione 3 Gestione Dello Stato3.4 Gestione dello stato (Provider)

Gestione dello stato con Provider

Perché passare a tecniche avanzate?

Con l’aumentare della complessità di un’app Flutter, setState() non basta più.

Limiti di setState()

  • Funziona solo all’interno del widget dove lo stato è definito
  • Difficile da riusare o testare
  • Aumenta la complessità del codice quando lo stato deve essere condiviso tra più widget
  • Nessuna separazione tra UI e logica di business

Esempio logico

Immagina di creare un’app con una schermata di elenco prodotti e una schermata carrello. Ogni volta che un utente aggiunge un prodotto al carrello:

  • la schermata di elenco deve aggiornare il pulsante “Aggiunto”
  • la AppBar deve mostrare il numero di articoli aggiornato
  • la schermata carrello deve riflettere i cambiamenti

Con setState() non puoi gestire tutto questo facilmente, perché ogni schermata vive nel suo stato locale. Serve uno stato condiviso. Entra in gioco Provider.

Hello

Cos’è Provider?

Provider consente:

  • Accesso semplice e centralizzato allo stato condiviso
  • Separazione tra la logica e la UI
  • Riutilizzo del codice
  • Scalabilità e testabilità

Basato su InheritedWidget, ma reso semplice da un’API ad alto livello.


Tipi principali di Provider

  • Provider<T>: legge un valore statico
  • ChangeNotifierProvider<T>: per oggetti che estendono ChangeNotifier
  • Consumer e Selector: per reagire ai cambiamenti

ChangeNotifier: modello osservabile semplice

Esempio: Contatore

class Counter extends ChangeNotifier { int value = 0; void increment() { value++; notifyListeners(); } }

Integrazione nel widget tree

ChangeNotifierProvider( create: (_) => Counter(), child: MyApp(), )

Hello


Nota sulla Dependency Injection

In questo esempio, stiamo iniettando la dipendenza (Counter) all’interno dell’albero dei widget. Questo è un concetto fondamentale noto come Dependency Injection: invece di creare oggetti direttamente dove servono, li forniamo dall’esterno in modo centralizzato. Questo approccio:

  • migliora la modularità
  • rende il codice più testabile
  • permette un controllo più preciso sul ciclo di vita degli oggetti

Quando la tua app inizia a gestire più stati (es. carrello, utente, preferenze), è consigliabile utilizzare MultiProvider per organizzare tutto in un unico punto.

Esempio: MultiProvider con CartProvider e UserProvider

MultiProvider( providers: [ ChangeNotifierProvider(create: (_) => CartProvider()), ChangeNotifierProvider(create: (_) => UserProvider()), ], child: MyApp(), );
  • CartProvider: Gestisce i prodotti nel carrello.
  • UserProvider: Gestisce le informazioni dell’utente (nome, email, ecc.).

Accesso allo stato

Con Consumer

Consumer<Counter>( builder: (context, counter, _) => Text('${counter.value}'), )

Cos’è Consumer?

Consumer<T> è un widget fornito da provider che consente di accedere allo stato e ricostruire solo il widget definito nel suo builder quando lo stato cambia.

È particolarmente utile per limitare i rebuild a una porzione specifica della UI, mantenendo il resto dell’interfaccia stabile.


Quando usarlo

  • Quando vuoi aggiornare solo una parte della UI senza ricostruire l’intero widget padre.
  • Quando hai un widget complesso o costoso da ricostruire, e vuoi isolare l’impatto delle modifiche di stato.
  • Quando sei in un widget esterno e vuoi accedere allo stato in modo esplicito.

Esempio pratico

Column( children: [ Text('Titolo statico'), Consumer<Counter>( builder: (context, counter, _) => Text('Contatore: ${counter.value}'), ), ], )

In questo esempio, solo il Text all’interno del Consumer verrà ricostruito quando il valore del contatore cambia, mentre il resto della colonna resterà invariato.


Con context.watch() / context.read()

final counter = context.watch<Counter>(); // per rebuild context.read<Counter>().increment(); // solo azione

Cos’è context.watch()?

context.watch<T>() restituisce l’istanza del provider di tipo T e registra il widget come ascoltatore. Questo significa che ogni volta che viene chiamato notifyListeners(), il widget viene ricostruito.

Quando usarlo

  • Quando hai bisogno di accedere ai dati e vuoi che il widget si aggiorni automaticamente al cambiare dello stato.
  • Tipico in build() di un widget.

Esempio

@override Widget build(BuildContext context) { final counter = context.watch<Counter>(); return Text('Contatore: ${counter.value}'); }

Cos’è context.read()?

context.read<T>() restituisce l’istanza del provider senza ascoltarla. È utile quando vuoi solo eseguire un’azione (es. chiamare un metodo) senza causare rebuild.

Quando usarlo

  • In eventi come onPressed, onTap, initState, dove non ti serve aggiornare la UI.
  • Quando vuoi separare l’azione dalla visualizzazione.

Esempio

ElevatedButton( onPressed: () { context.read<Counter>().increment(); }, child: Text('Aggiungi'), )

Con Selector

Selector<Counter, int>( selector: (_, counter) => counter.value, builder: (_, value, __) => Text('$value'), )

Cos’è Selector e quando usarlo

Selector è una variante avanzata di Consumer che permette di ascoltare solo una parte specifica dello stato.

È utile quando:

  • Hai un oggetto complesso, ma ti interessa solo un campo
  • Vuoi evitare rebuild inutili di tutto il widget
  • Vuoi migliorare le performance in app grandi

Esempio pratico

Hai uno stato con più proprietà:

class UserState extends ChangeNotifier { String name = "Alice"; int age = 30; void birthday() { age++; notifyListeners(); } }

Usando Consumer<UserState>, ogni cambiamento (anche solo name) farà ricostruire il widget. Con Selector, puoi ascoltare solo age:

Selector<UserState, int>( selector: (_, user) => user.age, builder: (_, age, __) => Text('Età: $age'), )

In questo modo, il widget viene ricostruito solo quando cambia age, e non quando cambia name.

Appunto: usare Provider.of<T>(context)

Un’altra alternativa è Provider.of<T>(context), che può essere utile quando:

  • Sei fuori dalla UI dichiarativa (es. in initState)
  • Vuoi accesso immediato all’istanza senza usare Consumer, watch o read

Per default, questo metodo triggera un rebuild se usato con listen: true.

@override void initState() { super.initState(); final counter = Provider.of<Counter>(context, listen: false); counter.increment(); }

Ricorda:

  • Usa listen: false se non vuoi che il widget venga ricostruito
  • Preferisci context.read() in UI interattiva moderna (più leggibile)

Tabella comparativa: modalità di accesso allo stato

MetodoRebuild?SintassiQuando usarlo
context.watch<T>()✅ Sìcontext.watch<Counter>()Quando vuoi che il widget si aggiorni al cambio di stato
context.read<T>()❌ Nocontext.read<Counter>()Quando vuoi solo eseguire un’azione (es. in onPressed)
Provider.of<T>(context)✅/❌ (config)Provider.of<Counter>(context, listen: false)Accesso manuale, adatto in initState o dispose
Consumer<T>✅ SìConsumer<Counter>(...)Per aggiornare solo una parte specifica della UI
Selector<T, R>✅ Sì (mirato)Selector<Counter, int>(...)Per evitare rebuild se cambia solo una parte dello stato

Nota: Usa listen: false con Provider.of per evitare rebuild.


Quando usare Provider?

Usalo quando:

  • Hai bisogno di condividere stato tra più widget
  • Vuoi mantenere la UI separata dalla logica
  • L’app cresce in complessità
  • Vuoi riutilizzare, testare e organizzare meglio il codice

Buone pratiche

  • Evita di usare troppi Consumer nidificati
  • Organizza i tuoi Provider alla radice dell’app
  • Usa Selector per ottimizzare i rebuild
  • Mantieni la logica fuori dai widget

Esercizi

1. Contatore con Provider

Crea un’app con:

  • ChangeNotifier
  • ChangeNotifierProvider
  • Consumer per aggiornare la UI

2. Aggiungi un bottone “reset”

Estendi il contatore per includere un metodo reset() in una pagina settings.

3. Todo List con Provider

Obiettivo:

Creare una Todo List che gestisca lo stato con Provider e ChangeNotifier.

  • Aggiungere un todo
  • Rimuovere un todo
  • Segnare un todo come completato

Requisiti:

  • TodoProvider: Classe che estende ChangeNotifier per gestire lo stato della lista.
  • Todo: Modello dati per un singolo task con id, title e isCompleted.
  • Consumer: Per aggiornare solo i widget interessati dal cambiamento dello stato.
  • context.read() e context.watch() per gestire le azioni.

Estensioni:

  • Implementare un filtro per mostrare solo i task completati/non completati.
  • Aggiungere un campo di input per il titolo del todo.

Esercizio: Carrello della Spesa Avanzato

Obiettivo:

Implementare un carrello della spesa avanzato che gestisca lo stato globale utilizzando Provider e ChangeNotifier. L’applicazione deve includere le seguenti funzionalità:

  • Aggiungere un prodotto al carrello con un pulsante dedicato.
  • Incrementare e decrementare la quantità di un prodotto nel carrello.
  • Rimuovere un prodotto con la possibilità di ripristinarlo tramite Undo.
  • Visualizzare il totale aggiornato in tempo reale.
  • Gestire la navigazione tra la schermata dei prodotti e il carrello tramite una bottom navigation bar.
  • Badge dinamico sul carrello per mostrare il numero di prodotti totali.

Requisiti:

  • CartProvider: Classe che estende ChangeNotifier per gestire lo stato del carrello.

  • Deve mantenere una lista di prodotti (_cart) e una cronologia (_history) per implementare l’Undo.

  • Implementare i metodi addProduct(), removeProduct(), incrementQuantity() e decrementQuantity().

  • Product: Modello dati per un singolo prodotto con i campi id, name, price e quantity.

  • Snackbar: Visualizzata ogni volta che un prodotto viene rimosso. Deve includere il pulsante UNDO per ripristinare il prodotto.

  • Selector: Utilizzato per aggiornare solo il contatore dei prodotti nel badge senza ricostruire l’intera schermata.


Struttura dell’App:

  • HomeScreen: Schermata principale che include la bottom navigation bar per navigare tra ProductScreen e CartScreen.

  • ProductScreen:

  • Lista di prodotti con pulsanti per aggiungerli al carrello.

  • CartScreen:

  • Lista dei prodotti nel carrello con pulsanti per incrementare, decrementare o rimuovere un prodotto.

  • Snackbar per l’azione di Undo.


Suggerimenti per l’implementazione:

  • Usa context.read() per eseguire azioni (incremento, decremento, rimozione).
  • Usa context.watch() per aggiornare dinamicamente i widget in base ai cambiamenti di stato.
  • Implementa la logica Undo utilizzando una lista _history per salvare lo stato precedente.