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
.
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 estendonoChangeNotifier
Consumer
eSelector
: 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 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
oread
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
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
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 estendeChangeNotifier
per gestire lo stato della lista.Todo
: Modello dati per un singolo task conid
,title
eisCompleted
.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 estendeChangeNotifier
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()
edecrementQuantity()
. -
Product
: Modello dati per un singolo prodotto con i campiid
,name
,price
equantity
. -
Snackbar
: Visualizzata ogni volta che un prodotto viene rimosso. Deve includere il pulsanteUNDO
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
eCartScreen
. -
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.