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
AppBardeve 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.
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 staticoChangeNotifierProvider<T>: per oggetti che estendonoChangeNotifierConsumereSelector: 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(),
)
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 azioneCos’è 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,watchoread
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: falsese non vuoi che il widget venga ricostruito - Preferisci
context.read()in UI interattiva moderna (più leggibile)
Tabella comparativa: modalità di accesso allo stato
| Metodo | Rebuild? | Sintassi | Quando usarlo |
|---|---|---|---|
context.watch<T>() | ✅ Sì | context.watch<Counter>() | Quando vuoi che il widget si aggiorni al cambio di stato |
context.read<T>() | ❌ No | context.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
Consumernidificati - Organizza i tuoi
Provideralla radice dell’app - Usa
Selectorper ottimizzare i rebuild - Mantieni la logica fuori dai widget
Esercizi
1. Contatore con Provider
Crea un’app con:
ChangeNotifierChangeNotifierProviderConsumerper 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 estendeChangeNotifierper gestire lo stato della lista.Todo: Modello dati per un singolo task conid,titleeisCompleted.Consumer: Per aggiornare solo i widget interessati dal cambiamento dello stato.context.read()econtext.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 estendeChangeNotifierper 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()edecrementQuantity(). -
Product: Modello dati per un singolo prodotto con i campiid,name,priceequantity. -
Snackbar: Visualizzata ogni volta che un prodotto viene rimosso. Deve includere il pulsanteUNDOper 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
ProductScreeneCartScreen. -
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
Undoutilizzando una lista_historyper salvare lo stato precedente.
