Cosa è lo stato e perché è necessario gestirlo
In Flutter, “stato” rappresenta tutte le informazioni che possono cambiare nel tempo in un’applicazione.
Fino ad ora abbiamo costruito la nostra applicazione non preoccupandoci di immagazzinare o rappresentare informazioni o di rendere dinamico il layout. La modalità di sviluppo del layout dichiarativa si combina perfettamente al valore chiave di organizzare i nostri dati con una singola sorgente della verità (single source of truth), con questo intendiamo salvare i dati un unico posto in modo tale da non creare inconsistenza e semplificare la logica.
Questo è un concetto ampio che non si limita solo a Flutter ma ad esempio a tutti i framework web.
Come la gestione dello stato diventa rilevante in Flutter
Come abbiamo visto esistono due tipi di Widget che possiamo costruire, StatelessWidget e StatefulWidget e proprio in quest’ultimo baseremo la lezione.
Nel momento in cui necessitiamo di modificare un valore a video in runtime dobbiamo estendere il nostro widget con la classe StatefulWidget, questo ci fornisce alcune funzioni utili che una volta richiamate
informano il framework che qualcosa nei dati è cambiata e che un widget considerato dirty
necessita di essere renderizzato nuovamente.
Tecniche base di gestione dello stato
Metodo | Descrizione |
---|---|
setState | Semplice, per widget locali, non adatto per stati condivisi |
ValueNotifier | Più efficiente di setState , ascoltabile tramite ValueListenableBuilder |
Primo metodo per aggiornare un valore nella UI: setState()
- Segnala a Flutter che qualcosa è cambiato
- Flutter marca il widget come “dirty”
- Quando parte un nuovo frame viene richiamato il metodo build per quel widget e per i soli sotto widget stateful o che ricevono parametri dal padre (e quindi devono essere ricostruiti con valori aggiornati).
setState(() {
counter++;
});
Esempio
// Definizione di un widget con stato (StatefulWidget)
class MyWidget extends StatefulWidget {
// Override del metodo createState per associare lo stato a questo widget
@override
_MyWidgetState createState() => _MyWidgetState();
}
// Classe che rappresenta lo stato del widget MyWidget
class _MyWidgetState extends State<MyWidget> {
// Variabile di stato: tiene traccia del contatore
int counter = 0;
// Funzione che incrementa il contatore e aggiorna il widget
void increment() {
setState(() {
counter++; // modifica lo stato: aumenta di 1
});
// setState notifica Flutter che lo stato è cambiato e che il widget va ricostruito
}
// Metodo build: costruisce l'interfaccia utente del widget
@override
Widget build(BuildContext context) {
return Column(
children: [
// Mostra il valore attuale del contatore
Text('$counter'),
// Pulsante che, quando premuto, chiama la funzione increment
ElevatedButton(
onPressed: increment,
child: Text('Increment'),
),
],
);
}
}
Esercizi
1. Bottom Navigation Bar
Crea un’app con uno StatefulWidget
e una BottomNavigationBar
che permette di cambiare schermata.
Suggerimento: cambia un valore intero selectedIndex
con setState()
e mostra un Widget
diverso a seconda del valore.
2. Contatore con reset
Aggiungi due pulsanti:
- uno che incrementa il contatore,
- uno che lo azzera (
reset
).
3. Switch attivo/inattivo
Aggiungi uno Switch
che controlla uno stato booleano (attivo
) e mostra un Text
che cambia tra "Attivo"
e "Inattivo"
.
4. Lista dinamica di elementi
Crea una ListView
che mostra una lista di stringhe.
Aggiungi un pulsante “Aggiungi elemento” che con setState()
aggiunge un nuovo elemento alla lista.
ValueNotifier
e ValueListenableBuilder
ValueNotifier<T>
è una classe Flutter che consente di notificare automaticamente i listener ogni volta che il suo valore cambia.
È una soluzione leggera per gestire stato reattivo senza usare setState()
o librerie più strutturate come Provider
o Riverpod
.
Creazione
final ValueNotifier<int> counter = ValueNotifier(0);
Visualizzazione reattiva
Puoi usare ValueListenableBuilder
per costruire un widget che si aggiorna automaticamente quando il ValueNotifier
cambia:
ValueListenableBuilder<int>(
valueListenable: counter,
builder: (context, value, _) {
return Text('Contatore: $value');
},
)
valueListenable
: è ilValueNotifier
che vuoi ascoltare.builder
: viene chiamato ogni volta che.value
cambia.
Aggiornare il valore
counter.value++;
Non è necessario usare setState()
: la UI si aggiorna automaticamente grazie a ValueListenableBuilder
.
Quando usarlo
- Quando gestisci una singola variabile reattiva all’interno di un widget.
- Per componenti incapsulati che devono reagire a cambiamenti interni.
- In contesti dove vuoi evitare ricostruzioni globali.
Quando evitarlo
- Se hai più variabili di stato o dipendenze complesse.
- Se lo stato deve essere condiviso tra widget diversi o a più livelli.
Esempio completo
class CounterWidget extends StatelessWidget {
final ValueNotifier<int> counter = ValueNotifier(0);
@override
Widget build(BuildContext context) {
return Column(
children: [
ValueListenableBuilder<int>(
valueListenable: counter,
builder: (context, value, _) => Text('$value'),
),
ElevatedButton(
onPressed: () => counter.value++,
child: Text('Incrementa'),
),
],
);
}
}
Esercizi
1. Contatore con reset
Crea un widget con:
- un pulsante “Incrementa”
- un pulsante “Reset”
che usano un
ValueNotifier<int>
per aggiornare un contatore.
2. Dark mode con ValueNotifier
Crea un’app che gestisce il tema scuro/chiaro tramite un ValueNotifier<bool>
, attivato da un’icona nella AppBar
.
3. Selettore di colore
Crea un’app in cui un ValueNotifier<Color>
controlla il colore di sfondo di un Container
.
Aggiungi dei pulsanti per cambiare il colore in tempo reale.
4. Toggle visibilità
Usa un ValueNotifier<bool>
per mostrare/nascondere un widget (Text
, Container
, ecc.) con un semplice interruttore.
5. Simula un loader
Usa un ValueNotifier<bool>
per simulare un caricamento:
- un pulsante “Carica dati”
- mostra un
CircularProgressIndicator
per 2 secondi - poi mostra i dati caricati
Ciclo di Vita di un StatefulWidget
- createState(): crea l’istanza dello stato associato al widget.
- initState(): inizializza lo stato; chiamato una sola volta.
- didChangeDependencies(): chiamato quando il widget dipende da un oggetto InheritedWidget.
- build(): costruisce la UI; chiamato ogni volta che lo stato cambia.
- didUpdateWidget(): chiamato quando il widget viene ricostruito con una nuova configurazione.
- dispose(): pulisce le risorse; chiamato quando il widget viene rimosso dal tree.
@override
void initState() {
super.initState();
// inizializzazioni
}
@override
void dispose() {
// liberazione risorse
super.dispose();
}
initState()
Quando si usa?
- Per inizializzare dati che non dipendono da BuildContext.
- Per startare animazioni, controller, stream.
Casi pratici:
- Avvio di un AnimationController.
- Chiamate iniziali a database/API.
- Setup di listener (es: su ScrollController, TextEditingController).
@override
void initState() {
super.initState();
_controller = AnimationController(vsync: this);
}
dispose()
Quando si usa?
- Per chiudere risorse: controller, stream, animazioni, focus nodes.
Casi pratici:
- AnimationController.dispose()
- StreamController.close()
@override
void dispose() {
_controller.dispose();
super.dispose();
}
Importante: mai usare context.watch() in initState()! (context non è ancora pronto.)
Demo: Widget con AnimationController
Non liberare le risorse in dispose()
può causare memory leak e app crash.
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
home: AnimationExample(),
);
}
}
class AnimationExample extends StatefulWidget {
const AnimationExample({super.key});
@override
State<AnimationExample> createState() => _AnimationExampleState();
}
class _AnimationExampleState extends State<AnimationExample> with SingleTickerProviderStateMixin {
late AnimationController _controller;
@override
void initState() {
super.initState();
_controller = AnimationController(
vsync: this,
duration: const Duration(seconds: 2),
)..repeat(reverse: true);
}
@override
void dispose() {
_controller.dispose(); // Importante: liberare risorse!
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Animation Example'),
),
body: Center(
child: FadeTransition(
opacity: _controller,
child: const FlutterLogo(size: 100),
),
),
);
}
}
Keyword late
Permette di dire a dart che quella variabile non sarà null ma sarà inizializzata in futuro così da gestire il null-safety