Refactoring del codice Flutter con const, keys e widget separati per ottimizzare le performance

Foto di Julia Taubitz su Unsplash

Best practiceIntermedio35 min Flutter 3.x

Refactoring del codice Flutter con const, keys e widget separati per ottimizzare le performance

Perché ottimizzare i rebuild

Uno dei problemi più comuni nelle app Flutter è la ricostruzione (rebuild) eccessiva dei widget, che degrada le performance e rende il codice difficile da mantenere.

In questo tutorial vedremo tre tecniche pratiche di refactoring che ogni sviluppatore Flutter dovrebbe padroneggiare:

  • L'uso di const per evitare rebuild inutili
  • L'uso corretto delle Key per preservare lo stato
  • La separazione dei widget in classi dedicate

Alla fine avrai una checklist mentale da applicare a ogni tua app per scrivere codice più performante e leggibile.

  1. 1

    Individuare i rebuild con il DevTools e debugPrint

    Prima di ottimizzare, dobbiamo misurare. Il modo più semplice per capire quante volte un widget viene ricostruito è stampare un messaggio nel metodo build.

    Apri anche il Flutter DevTools (Performance e Widget rebuild stats) per avere una visione grafica dei rebuild. Attiva la voce Track Widget Rebuilds dal menu Performance.

    Creiamo una piccola app di esempio con un contatore e un widget "costoso" che vogliamo monitorare.

    class CounterPage extends StatefulWidget {
      const CounterPage({super.key});
    
      @override
      State<CounterPage> createState() => _CounterPageState();
    }
    
    class _CounterPageState extends State<CounterPage> {
      int _count = 0;
    
      @override
      Widget build(BuildContext context) {
        debugPrint('build CounterPage');
        return Scaffold(
          appBar: AppBar(title: const Text('Rebuild demo')),
          body: Center(
            child: Column(
              mainAxisAlignment: MainAxisAlignment.center,
              children: [
                _ExpensiveHeader(),
                Text('Conteggio: $_count'),
              ],
            ),
          ),
          floatingActionButton: FloatingActionButton(
            onPressed: () => setState(() => _count++),
            child: const Icon(Icons.add),
          ),
        );
      }
    }
    
    class _ExpensiveHeader extends StatelessWidget {
      @override
      Widget build(BuildContext context) {
        debugPrint('build _ExpensiveHeader');
        return const Padding(
          padding: EdgeInsets.all(16),
          child: Text('Header statico', style: TextStyle(fontSize: 24)),
        );
      }
    }

    Risultato atteso

    Nella console vedrai 'build _ExpensiveHeader' stampato a ogni tap sul FAB, anche se il suo contenuto non cambia mai.

  2. 2

    Applicare const per bloccare i rebuild inutili

    Il modo più immediato per evitare che un widget venga ricostruito è renderlo const. Un widget const viene istanziato una sola volta e Flutter lo riconosce come identico a quello precedente, saltando la ricostruzione.

    Affinché un widget possa essere const, il suo costruttore deve essere const e tutti i suoi parametri devono essere valori costanti a compile-time.

    Nel nostro esempio, _ExpensiveHeader non ha stato né parametri variabili: aggiungiamo un costruttore const e usiamolo come const _ExpensiveHeader().

    class _ExpensiveHeader extends StatelessWidget {
      const _ExpensiveHeader();
    
      @override
      Widget build(BuildContext context) {
        debugPrint('build _ExpensiveHeader');
        return const Padding(
          padding: EdgeInsets.all(16),
          child: Text('Header statico', style: TextStyle(fontSize: 24)),
        );
      }
    }
    
    // Nel build della pagina:
    // children: [
    //   const _ExpensiveHeader(),
    //   Text('Conteggio: $_count'),
    // ],

    Risultato atteso

    Ora 'build _ExpensiveHeader' viene stampato una sola volta all'avvio: i tap successivi sul FAB non lo ricostruiscono più.

  3. 3

    Attivare il linter per suggerire le const automaticamente

    Aggiungere manualmente const ovunque è tedioso. Fortunatamente esistono regole di lint che segnalano dove è possibile inserirlo.

    Apri (o crea) il file analysis_options.yaml nella root del progetto e attiva le regole prefer_const_constructors, prefer_const_literals_to_create_immutables e prefer_const_constructors_in_immutables.

    L'IDE mostrerà un avviso e potrai applicare la fix automatica con dart fix --apply.

    include: package:flutter_lints/flutter.yaml
    
    linter:
      rules:
        prefer_const_constructors: true
        prefer_const_literals_to_create_immutables: true
        prefer_const_constructors_in_immutables: true
        prefer_const_declarations: true

    Risultato atteso

    Eseguendo 'dart fix --apply' da terminale, il framework aggiunge automaticamente i const mancanti in tutto il progetto.

  4. 4

    Separare i widget in classi invece di usare metodi helper

    Un anti-pattern comune è estrarre parti di UI in metodi helper (es. Widget _buildHeader()). Questi metodi NON creano un nuovo elemento nell'albero: vengono rieseguiti ogni volta che il widget genitore fa il build.

    La best practice è estrarre l'UI in classi widget dedicate. In questo modo Flutter può ottimizzare i rebuild in modo indipendente e, se il widget è const, evitarli del tutto.

    Confrontiamo i due approcci.

    // ❌ ANTI-PATTERN: metodo helper, si ricostruisce sempre
    Widget _buildFooter() {
      debugPrint('build footer helper');
      return const Text('Footer');
    }
    
    // ✅ PREFERIBILE: classe widget separata e const
    class _Footer extends StatelessWidget {
      const _Footer();
    
      @override
      Widget build(BuildContext context) {
        debugPrint('build _Footer');
        return const Text('Footer');
      }
    }
    
    // Uso nel build:
    // const _Footer(),  // <-- non si ricostruisce

    Risultato atteso

    Il metodo helper stampa il log a ogni rebuild del genitore, mentre il widget in classe const viene costruito una sola volta.

  5. 5

    Usare le Key per preservare lo stato negli elenchi dinamici

    Quando riordini, inserisci o rimuovi elementi con stato da una lista, Flutter potrebbe associare lo stato all'elemento sbagliato perché confronta i widget per posizione e tipo.

    Le Key (in particolare ValueKey e ObjectKey) permettono a Flutter di identificare univocamente ogni widget, preservando correttamente lo stato durante le modifiche alla lista.

    Vediamo un elenco di card con stato che, senza key, perde il colore corretto quando riordiniamo gli elementi.

    class ColorTile extends StatefulWidget {
      final String id;
      final String label;
    
      const ColorTile({required this.id, required this.label})
          : super(key: const ValueKey('placeholder'));
    
      // Meglio passare la key dall'esterno:
      // ColorTile({required this.id, required this.label}) : super(key: ValueKey(id));
    
      @override
      State<ColorTile> createState() => _ColorTileState();
    }
    
    class _ColorTileState extends State<ColorTile> {
      Color _color = Colors.grey;
    
      @override
      Widget build(BuildContext context) {
        return ListTile(
          tileColor: _color,
          title: Text(widget.label),
          onTap: () => setState(() => _color = Colors.amber),
        );
      }
    }
    
    // Nella costruzione della lista, assegna sempre una key stabile:
    // children: items.map((e) => ColorTile(key: ValueKey(e.id), id: e.id, label: e.label)).toList(),

    Risultato atteso

    Con le ValueKey basate su un id stabile, dopo aver riordinato la lista ogni tile mantiene il proprio stato (colore) corretto.

  6. 6

    Isolare i rebuild con RepaintBoundary e ValueListenableBuilder

    Come tocco finale, isoliamo le porzioni di UI che cambiano frequentemente in modo da non ricostruire tutto l'albero.

    • ValueListenableBuilder ricostruisce solo il widget che dipende da un valore, lasciando invariato il resto.
    • RepaintBoundary crea un layer separato per la pittura, evitando che un ridisegno frequente costringa a ridipingere anche i widget vicini.

    Rifattorizziamo il contatore per aggiornare solo il testo, non l'intera pagina.

    class CounterPage extends StatelessWidget {
      CounterPage({super.key});
    
      final ValueNotifier<int> _count = ValueNotifier(0);
    
      @override
      Widget build(BuildContext context) {
        debugPrint('build CounterPage'); // ora eseguito una sola volta
        return Scaffold(
          appBar: AppBar(title: const Text('Rebuild ottimizzato')),
          body: Center(
            child: Column(
              mainAxisAlignment: MainAxisAlignment.center,
              children: [
                const _ExpensiveHeader(),
                RepaintBoundary(
                  child: ValueListenableBuilder<int>(
                    valueListenable: _count,
                    builder: (context, value, _) => Text('Conteggio: $value'),
                  ),
                ),
              ],
            ),
          ),
          floatingActionButton: FloatingActionButton(
            onPressed: () => _count.value++,
            child: const Icon(Icons.add),
          ),
        );
      }
    }

    Risultato atteso

    Toccando il FAB si ricostruisce solo il Text del conteggio: 'build CounterPage' non viene più stampato, dimostrando l'isolamento del rebuild.

CondividiXLinkedInFacebookWhatsApp

Commenti (0)

Ancora nessun commento. Inizia tu!