Skip to Content
FlutterLezione 1 Introduzione a Flutter1.4 - Approfondimenti su Dart

Approfondimento su Dart: Funzioni, Operazioni, Tipizzazione, Null Safety, Asincronismo e Collezioni

Questa lezione offre un approfondimento su Dart, il linguaggio alla base di Flutter, presentando esempi pratici e commentati per chiarire concetti fondamentali ed avanzati.


Introduzione a Dart

Dart è un linguaggio open source sviluppato da Google. Ha una sintassi in stile C, è orientato agli oggetti ed utilizza un garbage collector. Supporta concetti avanzati quali interfacce, mixin, classi astratte, generics e type inference, rendendolo ideale per la creazione di applicazioni moderne, in particolare con Flutter.

Storia di Dart

Creato da Lars Bak e Kasper Lund e presentato nel 2011, Dart ha visto la sua prima versione stabile nel 2013. Originariamente progettato per essere eseguito su una VM in Chrome (idea poi abbandonata), Dart si è evoluto verso la compilazione in JavaScript e, successivamente, in codice nativo. Le versioni successive hanno introdotto le seguenti che vale la pena ricordare:

  • Un sistema di tipi più rigoroso.
  • La compilazione nativa multipiattaforma tramite dart2native.
  • L’integrazione con Flutter.
  • Con Dart 3.0, l’introduzione della null safety, record, pattern matching e nuovi modificatori di classe.
  • Con Dart 3.4, il supporto alla compilazione in WebAssembly.

💡
Tip

Una volta creato il progetto Hello World potete creare un file example.dart, nella root principale o all’interno di una cartella examples, dove potete testare i vari concetti

1. Funzioni e Uso di print()

La funzione print() è utilizzata per inviare output alla console, fondamentale per il debugging.

// Punto di ingresso dell'applicazione void main() { // Stampa un messaggio di benvenuto print("Benvenuto in Dart!"); // Dichiarazione e utilizzo di una variabile con interpolazione int numero = 42; print("Il valore di numero è: $numero"); }

2. Operazioni Matematiche e Logiche

Operazioni Matematiche

Dart supporta operazioni aritmetiche di base, utili per eseguire calcoli.

void main() { int a = 10; int b = 3; // Esempi di operazioni matematiche print("Somma: ${a + b}"); // 10 + 3 = 13 print("Differenza: ${a - b}"); // 10 - 3 = 7 print("Moltiplicazione: ${a * b}"); // 10 * 3 = 30 print("Divisione intera: ${a ~/ b}"); // 10 ~/ 3 = 3 (divisione intera) print("Divisione: ${a / b}"); // 10 / 3 ≈ 3.33 print("Modulo: ${a % b}"); // 10 % 3 = 1 (resto) }

Operazioni Logiche

Le operazioni logiche consentono di combinare condizioni booleane.

void main() { bool condizione1 = true; bool condizione2 = false; // Operatore AND: true solo se entrambe le condizioni sono vere print("AND: ${condizione1 && condizione2}"); // false // Operatore OR: true se almeno una condizione è vera print("OR: ${condizione1 || condizione2}"); // true // Operatore NOT: inverte il valore booleano print("NOT condizione1: ${!condizione1}"); // false }

3. Commentare il Codice

Documentare il codice è essenziale per migliorare la leggibilità e la manutenzione.

Commenti su una singola linea

// Questo è un commento su una singola linea. print("Codice eseguito."); // Commento inline

Commenti su più righe

/* Questo è un commento su più righe. Utile per spiegazioni dettagliate o per disabilitare temporaneamente blocchi di codice. */ print("Codice eseguito.");

4. Interpolazione di Stringhe con ${}

L’interpolazione permette di unire testo e variabili in modo chiaro.

void main() { String nome = "Luca"; int anni = 28; // Interpolazione semplice print("Ciao, sono $nome e ho $anni anni."); // Interpolazione con espressione int a = 7, b = 8; print("La somma di a e b è: ${a + b}"); // 7 + 8 = 15 }

5. Tipizzazione e Gestione delle Variabili

Dart è a tipizzazione statica; ogni variabile ha un tipo definito che aiuta a prevenire errori.

Tipi di Base

void main() { int eta = 30; // Intero double prezzo = 9.99; // Decimale String saluto = "Ciao"; // Stringa bool attivo = true; // Booleano print("$saluto, ho $eta anni e il prezzo è $prezzo. Attivo: $attivo"); }

var vs dynamic

void main() { var numero = 15; // Il tipo è inferito a int e non può essere cambiato // numero = "stringa"; // Errore: non è possibile cambiare il tipo dynamic variabile = 15; // Tipo dinamico, può cambiare a runtime variabile = "Ora sono una stringa"; print(variabile); }

final vs const

void main() { // 'final' è assegnato una sola volta e valutato in runtime final oggi = DateTime.now(); // 'const' è una costante nota a compile-time const pi = 3.1415; print("Oggi: $oggi"); print("Pi: $pi"); }

Mutabilità vs Immutabilità

void main() { var numeroMutabile = 20; numeroMutabile = 25; // Modifica consentita final numeroImmutabile = 30; // numeroImmutabile = 35; // Errore: variabile immutabile print("Mutabile: $numeroMutabile"); print("Immutabile: $numeroImmutabile"); }

Metodi Utili per Stringhe e Numeri

void main() { String frase = "Dart è fantastico!"; // Lunghezza della stringa print("Lunghezza: ${frase.length}"); // Conversione in maiuscolo print("Maiuscolo: ${frase.toUpperCase()}"); // Estrazione di una sottostringa print("Substring: ${frase.substring(0, 4)}"); // "Dart" // Formattazione dei numeri double num = 3.14159; print("Numero formattato: ${num.toStringAsFixed(2)}"); // "3.14" }

6. Controllo del Flusso e Operatori

Costrutti Condizionali

void main() { int x = 5; // Condizione if-else if (x > 0) { print("x è positivo"); } else if (x < 0) { print("x è negativo"); } else { print("x è zero"); } // Operatore ternario String risultato = (x > 0) ? "Positivo" : "Non positivo"; print("Risultato (ternario): $risultato"); // Esempi avanzati di switch in Dart // 1. Esempio base con lo switch (simile a JavaScript ma con regole più rigide) int day = 3; switch (day) { case 1: print("Lunedì"); break; case 2: print("Martedì"); break; case 3: print("Mercoledì"); break; default: print("Giorno non valido"); } // 2. Switch con raggruppamento dei casi // In Dart non c'è fall-through implicito come in JavaScript; // per raggruppare casi, è possibile elencarli insieme senza break tra di essi. String fruit = "Apple"; switch (fruit) { case "Apple": case "Mela": print("È una mela/Apple"); break; case "Banana": print("È una banana"); break; default: print("Frutto sconosciuto"); } // 3. Switch expression (Dart 3+) // Questa forma compatta consente di restituire direttamente un valore. int numero = 2; String descrizione = switch (numero) { 1 => "Uno", 2 => "Due", 3 => "Tre", _ => "Altro", }; print("Il numero è: \$descrizione"); // 4. Uso di pattern matching con lo switch // Dart permette di verificare il tipo della variabile direttamente nei case. Object input = "ciao"; switch (input) { case int i: print("È un intero: \$i"); break; case String s when s.isNotEmpty: print("È una stringa non vuota: \$s"); break; case String s: print("È una stringa vuota"); break; default: print("Tipo sconosciuto"); } }

Costrutti Iterativi

void main() { // Loop for tradizionale for (int i = 0; i < 3; i++) { print("Iterazione for: $i"); } // Loop for-in per iterare su una lista List<String> nomi = ["Anna", "Marco", "Luca"]; for (var nome in nomi) { print("Nome: $nome"); } // Loop while int j = 0; while (j < 3) { print("Iterazione while: $j"); j++; } // Loop do-while int k = 0; do { print("Iterazione do-while: $k"); k++; } while (k < 3); }
  • break Termina immediatamente l’esecuzione del ciclo in corso e passa al codice successivo dopo il ciclo.

  • continue Salta il resto del codice nel ciclo corrente e passa direttamente all’inizio della prossima iterazione.

Operatori di Decremento e Incremento

void main() { int a = 5; // Post-decremento: il valore viene restituito prima del decremento print("Post-decremento: ${a--}"); // Stampa 5, poi a diventa 4 a = 5; // Reset del valore // Pre-decremento: decrementa prima di restituire il valore print("Pre-decremento: ${--a}"); // a diventa 4 e stampa 4 // Esempi con incremento: a = 5; print("Post-incremento: ${a++}"); // Stampa 5, poi a diventa 6 a = 5; print("Pre-incremento: ${++a}"); // a diventa 6 e stampa 6 }

7. Null Safety

La null safety previene errori legati all’uso di valori null, obbligando a dichiarare esplicitamente variabili che possono essere null.

void main() { // Dichiarazione di una variabile nullable (può essere null) int? numeroNullable; print("Numero nullable iniziale: $numeroNullable"); // Stampa null // Operatore null-coalescing: se numeroNullable è null, usa il valore 0 int risultato = numeroNullable ?? 0; print("Risultato (null-coalescing): $risultato"); // Safe navigation: evita errori se la variabile è null print("Safe navigation: ${numeroNullable?.toString()}"); // Assegnazione di un valore e uso dell'operatore di null assertion numeroNullable = 20; print("Dopo assegnazione: ${numeroNullable!.toString()}"); // Usa '!' per affermare non essere null }

8. Funzioni Avanzate e Closures

Le funzioni in Dart sono oggetti di prima classe e possono essere passate o restituite come variabili.

<datatype> nome_funzione() {}

I parametri possono essere:

  • Posizionali obbligatori: I parametri vengono passati in ordine, e sono obbligatori.
  • Opzionali posizionali: Racchiusi tra parentesi quadre, possono essere omessi; è possibile assegnare loro un valore di default.
  • Nominali: Racchiusi tra parentesi graffe, possono essere passati specificando il nome del parametro. Possono essere resi obbligatori usando la keyword required.
// La funzione 'presentati' richiede che tutti i parametri nominali siano forniti, // grazie alla keyword 'required'. void presentati(int altezza, {required String nome, int? eta}) { print("Mi chiamo $nome e ho ${eta ?? 20} anni."); } void presentati(int altezza, [String? nome, int? eta]) { print("Mi chiamo $nome e ho ${eta ?? '20'} anni."); } void main() { presentati(173,nome: "Giulia", eta: 28); // Funziona correttamente // presentati(nome: "Giulia"); // Genera un errore: manca il parametro obbligatorio 'eta' }

Lambda

void main() { // Funzione anonima (lambda) come callback List<int> numeri = [1, 2, 3]; numeri.forEach((numero) { print("Numero: $numero"); }); //Uso della Lambda per Semplicità List<String> parole = ['dart', 'flutter', 'funzioni']; // Lambda compatta per trasformare e stampare ogni parola in maiuscolo parole.forEach((parola) => print(parola.toUpperCase())); //Detta anche arrow function in js }

Closure

Una closure è una funzione che “cattura” le variabili del contesto in cui è stata definita.

void main() { // Funzione che restituisce una closure che somma un addendo al valore fornito Function creaSommatore(int addendo) { return (int valore) => valore + addendo; } var sommaDi5 = creaSommatore(5); var sommaDi10 = creaSommatore(10); print("Somma di 10 con 5: ${sommaDi5(10)}"); // Output: 15 print("Somma di 10 con 10: ${sommaDi10(10)}"); // Output: 20 }

Le closure in Dart:

  • Permettono di creare funzioni che mantengono uno stato interno.
  • Consentono di scrivere codice modulare e riutilizzabile.
  • Sono particolarmente utili per callback ed eventi, dove è necessario “ricordare” il contesto in cui una funzione è stata creata.

Esempio Avanzato: Funzione che Ritorna un’Altra Funzione con Capturing

// Funzione che crea una funzione moltiplicatrice catturando il fattore Function creaMoltiplicatore(int fattore) { return (int valore) { int risultato = valore * fattore; print("Moltiplicando \$valore per \$fattore otteniamo \$risultato"); return risultato; }; } void main() { var moltiplicaPer3 = creaMoltiplicatore(3); var moltiplicaPer4 = creaMoltiplicatore(4); moltiplicaPer3(5); // Stampa: Moltiplicando 5 per 3 otteniamo 15 moltiplicaPer4(5); // Stampa: Moltiplicando 5 per 4 otteniamo 20 }

Passaggio di Funzioni come Argomenti

Le funzioni possono essere passate come parametri per creare comportamenti dinamici.

// Funzione che applica una funzione passata come parametro a un valore void applicaFunzione(int valore, int Function(int) funzione) { int risultato = funzione(valore); print("Risultato: \$risultato"); } int incrementa(int x) => x + 1; void main() { applicaFunzione(7, incrementa); // Output: 8 applicaFunzione(7, (x) => x * 2); // Output: 14 }

9. Record (Tuple)

I record (o tuple) in Dart sono strutture leggere che permettono di raggruppare insieme più valori di tipi differenti in un’unica entità, senza la necessità di definire una classe dedicata.

Caratteristiche Principali

  • Immutabilità
  • Accesso Posizionale $1, $2
  • Destructuring
  • Campi Nominati (Dart 3)

Sintassi di Base

var recordSemplice = (42, "Alice", true);

Dichiarazione del Tipo Record

Puoi specificare esplicitamente il tipo di un record quando ne definisci una funzione o una variabile:

(int, String, bool) getPersonInfo() { int age = 30; String name = "Alice"; bool isActive = true; return (age, name, isActive); }

Accesso agli Elementi del Record

I record offrono due modi principali di accesso ai valori:

1. Accesso Posizionale

void main() { var info = (30, "Alice", true); print("Età: \${info.$1}"); // Stampa: 30 print("Nome: \${info.$2}"); // Stampa: Alice print("Attivo: \${info.$3}"); // Stampa: true }

2. Destructuring

void main() { var (age, name, isActive) = getPersonInfo(); print("Età: \$age, Nome: \$name, Attivo: \$isActive"); }

Record con Campi Nominati

void main() { // Record che contiene un campo posizionale ed uno o più campi nominati var recordConNomine = (42, name: "Alice", isActive: true); // Accesso al valore posizionale print("Valore posizionale: \${recordConNomine.$1}"); // 42 // Accesso ai campi nominati print("Nome: \${recordConNomine.name}"); // Alice print("Attivo: \${recordConNomine.isActive}"); // true }
(int, {String name, bool isActive}) getPersonData() { int age = 30; String personName = "Alice"; bool active = true; return (age, name: personName, isActive: active); } void main() { var data = getPersonData(); print("Età: \${data.$1}"); print("Nome: \${data.name}"); print("Attivo: \${data.isActive}"); }

Sono particolarmente utili per:

  • Restituire più valori da una funzione in modo semplice e conciso.
  • Organizzare dati temporanei senza dover creare una classe dedicata.
  • Semplificare il codice grazie al destructuring e alla sintassi compatta.

10. Gestione degli Errori: try-catch-finally

Utilizza i blocchi try-catch-finally per gestire eccezioni e garantire l’esecuzione di codice di cleanup.

void main() { try { // Questo provocherà un errore: divisione per zero int risultato = 100 ~/ 0; print("Risultato: $risultato"); } catch (e) { // Gestione dell'errore: stampa l'eccezione print("Errore catturato: $e"); } finally { // Questo blocco viene eseguito sempre print("Blocco finally eseguito."); } }

Conclusioni

In questa lezione abbiamo esplorato:

  • Funzioni di base e l’uso di print()
  • Operazioni matematiche e logiche con esempi commentati
  • Tecniche di commento per migliorare la leggibilità del codice
  • Interpolazione di stringhe con template literals
  • Tipizzazione statica: uso di int, double, bool, String, var, dynamic, final e const
  • Metodi utili per manipolare stringhe e numeri
  • Controllo del flusso e uso degli operatori (inclusi incremento/decremento)
  • Null Safety e gestione delle variabili nullable
  • Funzioni avanzate, closures e funzioni anonime
  • Record (Tuple)
  • Gestione degli errori con try-catch-finally

Questi concetti costituiscono una base solida per lo sviluppo di applicazioni Flutter. Approfondire questi argomenti aiuta a scrivere codice più sicuro, leggibile ed efficiente.

Buon studio e buon coding!