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-casesemplifica 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.