Skip to Content
FlutterLezione 3 Gestione Dello Stato3.1 Gestione dello stato (base)

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

MetodoDescrizione
setStateSemplice, per widget locali, non adatto per stati condivisi
ValueNotifierPiù 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: è il ValueNotifier 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