Introduzione

Con Dart 3 il linguaggio alla base di Flutter ha compiuto un salto qualitativo enorme grazie all'introduzione dei records, del pattern matching e del destructuring. Si tratta di funzionalità che molti sviluppatori conoscono da altri linguaggi (Kotlin, Rust, Swift) e che finalmente permettono di scrivere codice Flutter più espressivo, riducendo boilerplate e migliorando la sicurezza a compile-time.

In questo articolo vediamo come usarli concretamente nelle nostre app.

I Records: tuple leggere e tipizzate

Un record è un tipo anonimo che aggrega più valori senza doverne dichiarare una classe. È immutabile e definito al volo.

// Record posizionale
(int, String) utente = (1, 'Anna');

// Record con campi nominati
({double lat, double lng}) posizione = (lat: 45.46, lng: 9.19);

print(utente.$1);      // 1
print(utente.$2);      // Anna
print(posizione.lat);  // 45.46

Il caso d'uso più utile è restituire più valori da una funzione senza creare una classe dedicata:

(String nome, int eta) parseUtente(Map<String, dynamic> json) {
  return (json['nome'] as String, json['eta'] as int);
}

final (nome, eta) = parseUtente(dati);
print('$nome ha $eta anni');

Destructuring: scomporre i dati

Il destructuring consente di estrarre i valori da record, liste e mappe assegnandoli direttamente a variabili.

// Destructuring di una lista
final [primo, secondo, ...resto] = [1, 2, 3, 4, 5];
print(primo);  // 1
print(resto);  // [3, 4, 5]

// Destructuring di una mappa
final {'nome': nome, 'citta': citta} = {
  'nome': 'Luca',
  'citta': 'Roma',
};

È possibile scambiare due variabili senza variabile temporanea:

var a = 1;
var b = 2;
(a, b) = (b, a);

Pattern matching con switch

Il switch in Dart 3 diventa un'espressione potentissima grazie ai pattern. Non serve più il break e può restituire un valore.

String descriviStato(int codice) {
  return switch (codice) {
    200 => 'OK',
    404 => 'Non trovato',
    >= 500 => 'Errore server',
    _ => 'Sconosciuto',
  };
}

Possiamo usare pattern anche su record, ideale per gestire coordinate o coppie di valori:

String quadrante((int, int) punto) {
  return switch (punto) {
    (0, 0) => 'Origine',
    (var x, var y) when x > 0 && y > 0 => 'Primo quadrante',
    (var x, _) when x < 0 => 'A sinistra',
    _ => 'Altrove',
  };
}

La clausola when aggiunge condizioni (guard clauses) ai pattern.

Applicazione pratica in Flutter: gestione degli stati

Un caso reale è la gestione degli stati asincroni. Combinando pattern matching e sealed class otteniamo un codice espressivo e sicuro:

sealed class RisultatoUtente {}
class Caricamento extends RisultatoUtente {}
class Successo extends RisultatoUtente {
  final String nome;
  Successo(this.nome);
}
class Errore extends RisultatoUtente {
  final String messaggio;
  Errore(this.messaggio);
}

Widget build(BuildContext context) {
  final RisultatoUtente stato = _stato;
  return switch (stato) {
    Caricamento() => const CircularProgressIndicator(),
    Successo(nome: final n) => Text('Benvenuto $n'),
    Errore(messaggio: final m) => Text('Errore: $m'),
  };
}

Grazie a sealed, il compilatore verifica che tutti i casi siano gestiti: se aggiungi un nuovo stato senza gestirlo, ottieni un errore a compile-time. Questo elimina un'intera categoria di bug.

Object patterns e AsyncSnapshot

I pattern funzionano bene anche con classi esistenti come AsyncSnapshot dentro un FutureBuilder:

FutureBuilder<String>(
  future: _caricaDati(),
  builder: (context, snapshot) {
    return switch (snapshot) {
      AsyncSnapshot(hasError: true, :final error) =>
        Text('Errore: $error'),
      AsyncSnapshot(hasData: true, :final data) =>
        Text('Dati: $data'),
      _ => const CircularProgressIndicator(),
    };
  },
)

La sintassi :final data è uno shorthand per data: final data.

If-case: pattern nelle condizioni

Oltre allo switch, i pattern si usano anche con if-case:

final json = {'lat': 45.46, 'lng': 9.19};

if (json case {'lat': final double lat, 'lng': final double lng}) {
  print('Coordinate valide: $lat, $lng');
}

Questo è particolarmente utile per validare in modo sicuro i dati provenienti da API JSON, controllando struttura e tipi in un colpo solo.

Conclusioni

Records, pattern matching e destructuring rappresentano una delle evoluzioni più importanti di Dart. Portano vantaggi concreti:

  • Meno boilerplate: niente classi per restituire più valori.
  • Codice più leggibile: gli switch espressione sono compatti e chiari.
  • Maggiore sicurezza: con le sealed class il compilatore garantisce l'esaustività.
  • Validazione dei dati: if-case semplifica il parsing sicuro del JSON.

Adottare queste funzionalità nei progetti Flutter moderni significa scrivere codice più robusto e manutenibile. Vale la pena rifattorizzare gli switch tradizionali e le funzioni con parametri di output per sfruttarne appieno il potenziale.