Gestire il ciclo di vita dei widget in Flutter con StatefulWidget e mixin
Best practiceIntermedio35 min Flutter 3.x

Gestire il ciclo di vita dei widget in Flutter con StatefulWidget e mixin

Comprendere il ciclo di vita dei widget è fondamentale per scrivere app Flutter robuste ed efficienti. Ogni StatefulWidget attraversa una serie di fasi ben definite, dalla creazione dello stato alla sua distruzione. Sapere quando e dove eseguire operazioni come inizializzare controller, sottoscrivere stream o liberare risorse evita memory leak e comportamenti imprevisti.

In questo tutorial vedremo passo dopo passo tutti i metodi del ciclo di vita di uno State, quando vengono chiamati e a cosa servono. Aggiungeremo poi il mixin WidgetsBindingObserver per intercettare i cambi di stato dell'intera applicazione (foreground, background, chiusura). Al termine avrai un modello mentale chiaro per gestire correttamente le risorse nei tuoi widget.

  1. 1

    Creare uno StatefulWidget di base

    Partiamo creando un semplice StatefulWidget. Ricorda che un StatefulWidget è composto da due classi: il widget stesso (immutabile) e la sua classe State (mutabile), che contiene lo stato e la logica del ciclo di vita.

    Il metodo createState() viene chiamato una sola volta da Flutter quando il widget viene inserito nell'albero per la prima volta.

    import 'package:flutter/material.dart';
    
    class LifecycleDemo extends StatefulWidget {
      const LifecycleDemo({super.key, required this.title});
    
      final String title;
    
      @override
      State<LifecycleDemo> createState() => _LifecycleDemoState();
    }
    
    class _LifecycleDemoState extends State<LifecycleDemo> {
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          appBar: AppBar(title: Text(widget.title)),
          body: const Center(child: Text('Ciclo di vita')),
        );
      }
    }

    Risultato atteso

    Una schermata con AppBar e testo centrato. Il metodo build viene eseguito ogni volta che il widget deve essere ridisegnato.

  2. 2

    Inizializzare risorse con initState

    initState() viene chiamato una sola volta, subito dopo la creazione dell'oggetto State e prima del primo build. È il posto giusto per inizializzare controller, timer, animazioni o sottoscrivere stream.

    Regola d'oro: chiama sempre super.initState() come prima istruzione. Non puoi ancora accedere in modo sicuro a InheritedWidget (come Theme.of o MediaQuery.of) qui: per quello esiste didChangeDependencies.

    class _LifecycleDemoState extends State<LifecycleDemo> {
      late final TextEditingController _controller;
      int _rebuildCount = 0;
    
      @override
      void initState() {
        super.initState();
        debugPrint('initState: creo le risorse');
        _controller = TextEditingController();
      }
    
      @override
      Widget build(BuildContext context) {
        _rebuildCount++;
        return Scaffold(
          appBar: AppBar(title: Text(widget.title)),
          body: Padding(
            padding: const EdgeInsets.all(16),
            child: Column(
              children: [
                TextField(controller: _controller),
                Text('Build eseguiti: $_rebuildCount'),
              ],
            ),
          ),
        );
      }
    }

    Risultato atteso

    Nella console appare 'initState: creo le risorse' una sola volta all'apertura della schermata.

  3. 3

    Reagire ai cambi di dipendenze con didChangeDependencies

    didChangeDependencies() viene chiamato subito dopo initState (al primo caricamento) e ogni volta che cambia un InheritedWidget da cui il widget dipende (ad esempio quando cambia il tema o la lingua).

    È il punto corretto per leggere valori da Theme.of(context), MediaQuery.of(context) o provider ereditati, perché qui il context è pienamente disponibile.

    @override
    void didChangeDependencies() {
      super.didChangeDependencies();
      final brightness = Theme.of(context).brightness;
      debugPrint('didChangeDependencies: brightness = $brightness');
    }

    Risultato atteso

    Il log stampa la brightness corrente all'avvio e di nuovo se il tema dell'app cambia (es. passaggio a dark mode).

  4. 4

    Gestire l'aggiornamento del widget con didUpdateWidget

    Quando il widget padre si ricostruisce con nuovi parametri, Flutter riutilizza lo stesso oggetto State ma sostituisce l'istanza del widget. didUpdateWidget(oldWidget) ti permette di confrontare i vecchi valori con quelli nuovi (accessibili tramite widget) e reagire di conseguenza, ad esempio ricreando un controller basato su una proprietà cambiata.

    @override
    void didUpdateWidget(covariant LifecycleDemo oldWidget) {
      super.didUpdateWidget(oldWidget);
      if (oldWidget.title != widget.title) {
        debugPrint('Titolo cambiato: ${oldWidget.title} -> ${widget.title}');
      }
    }

    Risultato atteso

    Se il widget padre passa un nuovo titolo, viene stampato il confronto tra vecchio e nuovo valore senza ricreare lo State.

  5. 5

    Liberare le risorse con dispose

    dispose() viene chiamato una sola volta quando l'oggetto State viene rimosso permanentemente dall'albero. È fondamentale liberare qui tutte le risorse allocate in initState: controller, stream subscription, timer, animazioni.

    Dimenticare dispose è una delle cause più comuni di memory leak in Flutter. Chiama super.dispose() come ultima istruzione.

    @override
    void dispose() {
      debugPrint('dispose: libero le risorse');
      _controller.dispose();
      super.dispose();
    }

    Risultato atteso

    Uscendo dalla schermata viene stampato 'dispose: libero le risorse' e il TextEditingController viene distrutto correttamente.

  6. 6

    Intercettare lo stato dell'app con WidgetsBindingObserver

    Oltre al ciclo di vita del singolo widget, spesso serve reagire ai cambi di stato dell'intera applicazione (l'utente la mette in background, torna in foreground, la chiude). Il mixin WidgetsBindingObserver con l'override di didChangeAppLifecycleState permette proprio questo.

    Ricorda di registrare l'observer in initState e di rimuoverlo in dispose per evitare leak.

    class _LifecycleDemoState extends State<LifecycleDemo>
        with WidgetsBindingObserver {
      @override
      void initState() {
        super.initState();
        WidgetsBinding.instance.addObserver(this);
      }
    
      @override
      void didChangeAppLifecycleState(AppLifecycleState state) {
        super.didChangeAppLifecycleState(state);
        switch (state) {
          case AppLifecycleState.resumed:
            debugPrint('App in primo piano');
          case AppLifecycleState.inactive:
            debugPrint('App inattiva');
          case AppLifecycleState.paused:
            debugPrint('App in background');
          case AppLifecycleState.detached:
            debugPrint('App in chiusura');
          case AppLifecycleState.hidden:
            debugPrint('App nascosta');
        }
      }
    
      @override
      void dispose() {
        WidgetsBinding.instance.removeObserver(this);
        super.dispose();
      }
    
      @override
      Widget build(BuildContext context) => const SizedBox.shrink();
    }

    Risultato atteso

    Mettendo l'app in background o riportandola in primo piano, la console mostra i relativi messaggi di stato del ciclo di vita dell'applicazione.

  7. 7

    Riepilogo dell'ordine di esecuzione

    Ora hai il quadro completo. L'ordine tipico dei metodi è:

    1. createState() → crea l'oggetto State
    2. initState() → una volta, inizializza risorse
    3. didChangeDependencies() → dopo initState e ai cambi di InheritedWidget
    4. build() → ogni volta che serve ridisegnare
    5. didUpdateWidget() → quando il padre passa nuovi parametri
    6. dispose() → una volta, alla rimozione

    Best practice da ricordare:

    • Alloca risorse in initState, liberale in dispose.
    • Non chiamare setState dentro build o dispose.
    • Usa didChangeDependencies per leggere Theme, MediaQuery, provider.
    • Registra/rimuovi sempre gli observer in modo simmetrico.
    • Per operazioni asincrone in initState, controlla mounted prima di chiamare setState.
    // Esempio: async in initState con controllo di mounted
    @override
    void initState() {
      super.initState();
      _caricaDati();
    }
    
    Future<void> _caricaDati() async {
      final dati = await Future.delayed(
        const Duration(seconds: 1),
        () => 'Caricato',
      );
      if (!mounted) return; // evita setState su widget rimosso
      setState(() {
        // aggiorna lo stato con i dati
      });
    }

    Risultato atteso

    Hai un modello mentale completo del ciclo di vita e sai come gestire operazioni asincrone in sicurezza controllando la proprietà mounted.

CondividiXLinkedInFacebookWhatsApp

Commenti (0)

Ancora nessun commento. Inizia tu!