Dart essenziale per Flutter: variabili, funzioni, classi, async e await

Foto di Ehud Neuhaus su Unsplash

GuidePrincipiante35 min Flutter 3.x (Dart 3.x)

Dart essenziale per Flutter: variabili, funzioni, classi, async e await

Molti principianti aprono il primo progetto Flutter e si trovano davanti a righe come final List<String> items = []; oppure Future<void> load() async { ... } senza sapere davvero cosa stiano leggendo. Flutter è scritto in Dart, e conoscere le basi del linguaggio è la differenza tra copiare codice e capirlo.

In questo tutorial ripercorriamo il minimo indispensabile di Dart per lavorare bene con Flutter:

  • variabili, tipi e null safety
  • funzioni, parametri opzionali e nominati, arrow function
  • collezioni (List, Map) e cicli
  • classi, costruttori, final, getter ed ereditarietà
  • programmazione asincrona con Future, async/await e gestione degli errori

Ogni esempio è eseguibile in un file Dart puro (anche su dartpad.dev, senza installare nulla) e nell'ultimo passo vedremo come questi concetti si traducono in un widget Flutter reale.

Prerequisiti: nessuno, se usi DartPad. Se preferisci l'IDE, ti basta avere l'SDK Flutter installato.

  1. 1

    Preparare l'ambiente: il primo file Dart

    Per esercitarti non serve un progetto Flutter completo. Hai due strade:

    1. DartPad (consigliato per iniziare): vai su dartpad.dev, cancella il codice di esempio e incolla il tuo.
    2. Locale: crea un file bin/main.dart in una cartella qualsiasi e lancialo con dart run bin/main.dart (l'SDK Dart è incluso in Flutter).

    Ogni programma Dart parte dalla funzione main(). Per stampare a console si usa print().

    Nota: in Flutter la main() esiste sempre ed è quella che chiama runApp(). È lo stesso identico punto di ingresso che vedi qui.

    void main() {
      print('Ciao Dart!');
    
      // I commenti su una riga iniziano con //
      /* oppure si usano i commenti
         su più righe come questo */
    
      final anno = 2024;
      print('Siamo nel $anno'); // string interpolation
      print('Fra 2 anni: ${anno + 2}'); // espressione dentro ${}
    }

    Risultato atteso

    In console vedi: "Ciao Dart!", "Siamo nel 2024", "Fra 2 anni: 2026". Hai imparato anche la string interpolation con $ e ${}.

  2. 2

    Variabili, tipi e null safety

    Dart è tipizzato staticamente, ma può inferire il tipo. Le parole chiave da conoscere sono tre:

    • var: il tipo viene dedotto dal valore iniziale, ma la variabile resta riassegnabile.
    • final: assegnata una sola volta, a runtime. È quella che userai più spesso in Flutter.
    • const: costante nota già in fase di compilazione (in Flutter serve anche a ottimizzare i widget).

    Null safety

    Dal Dart 2.12 una variabile non può contenere null a meno che tu non lo dichiari esplicitamente aggiungendo ? al tipo. Questo elimina alla radice il classico crash "null reference".

    Operatori utili:

    Operatore Significato
    ? il tipo può essere null (String? nome)
    ?? valore di fallback se null (nome ?? 'Anonimo')
    ?. accesso sicuro (nome?.length)
    ! "so che non è null" — usalo con cautela
    late inizializzo dopo, ma prima dell'uso
    void main() {
      var citta = 'Roma';      // String inferita
      citta = 'Milano';        // ok: var è riassegnabile
    
      final utente = 'Anna';   // non riassegnabile
      const pi = 3.14;         // costante a compile-time
    
      int eta = 30;
      double altezza = 1.75;
      bool attivo = true;
    
      // Null safety
      String? soprannome;             // può essere null
      print(soprannome);              // null
      print(soprannome ?? 'nessuno'); // nessuno
      print(soprannome?.length);      // null, niente crash
    
      soprannome = 'Anni';
      print(soprannome.toUpperCase()); // ANNI
    
      // late: prometto di assegnarla prima dell'uso
      late String messaggio;
      messaggio = 'Benvenuta $utente';
    
      print('$messaggio ($eta anni, $altezza m, attivo: $attivo, pi=$pi, $citta)');
    }

    Risultato atteso

    La console stampa null, "nessuno", null, "ANNI" e la frase finale. Se provi a scrivere `String x = null;` l'editor segnala subito un errore di compilazione: è la null safety al lavoro.

  3. 3

    Funzioni: parametri posizionali, opzionali e nominati

    Una funzione in Dart dichiara il tipo di ritorno (void se non restituisce nulla) e i parametri.

    Esistono tre modi di passare i parametri:

    • posizionali obbligatori: saluta(String nome)
    • posizionali opzionali: tra parentesi quadre [int? volte]
    • nominati: tra graffe {required String nome, int volte = 1}

    I parametri nominati sono quelli che vedi ovunque in Flutter: Text('ciao', style: ..., textAlign: ...). La parola chiave required indica che il parametro nominato è obbligatorio.

    Se il corpo è una sola espressione, puoi usare la arrow syntax =>. Infine, in Dart le funzioni sono valori: puoi passarle come argomento (è ciò che accade con onPressed: () { ... }).

    // Parametri posizionali
    int somma(int a, int b) {
      return a + b;
    }
    
    // Arrow function (una sola espressione)
    int quadrato(int n) => n * n;
    
    // Parametri nominati, con default e required
    String descrivi({required String nome, int eta = 0, String? citta}) {
      final luogo = citta == null ? '' : ' di $citta';
      return '$nome, $eta anni$luogo';
    }
    
    // Funzione che riceve un'altra funzione (callback)
    void ripeti(int volte, void Function(int) azione) {
      for (var i = 1; i <= volte; i++) {
        azione(i);
      }
    }
    
    void main() {
      print(somma(3, 4));      // 7
      print(quadrato(5));      // 25
    
      print(descrivi(nome: 'Luca', eta: 28, citta: 'Torino'));
      print(descrivi(nome: 'Sara')); // eta usa il default
    
      ripeti(3, (i) => print('Giro numero $i'));
    }

    Risultato atteso

    Output: 7, 25, "Luca, 28 anni di Torino", "Sara, 0 anni", poi tre righe "Giro numero 1/2/3". Hai visto la stessa sintassi dei callback Flutter come onPressed.

  4. 4

    Collezioni e cicli: List, Map e i metodi che userai in Flutter

    Le due collezioni fondamentali sono:

    • List<T>: elenco ordinato (in Flutter diventa la lista di dati che mostri in una ListView).
    • Map<K, V>: coppie chiave/valore (tipico risultato del parsing di un JSON).

    Oltre ai cicli for e for-in, impara subito i metodi funzionali: map, where, firstWhere, any, reduce. In Flutter li userai continuamente, ad esempio con prodotti.map((p) => Text(p.nome)).toList().

    Utile anche il collection if e lo spread operator ..., che in Flutter servono per costruire liste di widget condizionali.

    void main() {
      final linguaggi = <String>['Dart', 'Kotlin', 'Swift'];
      linguaggi.add('JavaScript');
    
      for (final l in linguaggi) {
        print('- $l');
      }
    
      // map / where restituiscono Iterable: spesso serve .toList()
      final maiuscoli = linguaggi.map((l) => l.toUpperCase()).toList();
      final corti = linguaggi.where((l) => l.length <= 5).toList();
      print(maiuscoli);
      print(corti);
    
      // Map chiave/valore
      final utente = <String, dynamic>{
        'nome': 'Anna',
        'eta': 30,
        'admin': false,
      };
      print(utente['nome']);
      utente['citta'] = 'Bologna';
      print(utente.keys.toList());
    
      // Collection if e spread
      final mostraExtra = true;
      final menu = <String>[
        'Home',
        'Profilo',
        if (mostraExtra) 'Impostazioni',
        ...['Aiuto', 'Logout'],
      ];
      print(menu);
    }

    Risultato atteso

    Vedi la lista dei linguaggi riga per riga, gli array trasformati con map/where, il valore 'Anna' e le chiavi della Map, e infine il menu con 'Impostazioni' incluso perché mostraExtra è true.

  5. 5

    Classi, costruttori e modelli di dati

    Le classi in Dart servono soprattutto a creare i modelli della tua app (Utente, Prodotto, Articolo...).

    Punti chiave:

    • I campi final rendono l'oggetto immutabile: pratica consigliata in Flutter.
    • Il costruttore abbreviato Prodotto(this.nome, this.prezzo) assegna direttamente i campi.
    • I costruttori nominati (Prodotto.gratis()) offrono modi alternativi di creare l'oggetto; molto comune Prodotto.fromJson(Map).
    • I getter calcolano un valore al volo.
    • extends per l'ereditarietà, @override per ridefinire un metodo (come toString() o il build() dei widget!).

    Quando in Flutter scrivi class HomePage extends StatelessWidget stai usando esattamente questi meccanismi.

    class Prodotto {
      final String nome;
      final double prezzo;
      final int quantita;
    
      // Costruttore con parametri nominati
      const Prodotto({
        required this.nome,
        required this.prezzo,
        this.quantita = 1,
      });
    
      // Costruttore nominato: crea l'oggetto da una Map (tipico JSON)
      factory Prodotto.fromJson(Map<String, dynamic> json) {
        return Prodotto(
          nome: json['nome'] as String,
          prezzo: (json['prezzo'] as num).toDouble(),
          quantita: json['quantita'] as int? ?? 1,
        );
      }
    
      // Getter calcolato
      double get totale => prezzo * quantita;
    
      @override
      String toString() => '$nome x$quantita = ${totale.toStringAsFixed(2)} EUR';
    }
    
    class ProdottoScontato extends Prodotto {
      final double sconto; // 0.2 = 20%
    
      const ProdottoScontato({
        required super.nome,
        required super.prezzo,
        super.quantita,
        required this.sconto,
      });
    
      @override
      double get totale => super.totale * (1 - sconto);
    }
    
    void main() {
      final p = Prodotto(nome: 'Tastiera', prezzo: 49.90, quantita: 2);
      print(p);
    
      final s = ProdottoScontato(nome: 'Mouse', prezzo: 30, sconto: 0.25);
      print(s);
    
      final daJson = Prodotto.fromJson({'nome': 'Monitor', 'prezzo': 199});
      print(daJson);
    }

    Risultato atteso

    Console: "Tastiera x2 = 99.80 EUR", "Mouse x1 = 22.50 EUR", "Monitor x1 = 199.00 EUR". Hai creato un modello immutabile, un costruttore da JSON e una sottoclasse che sovrascrive un getter.

  6. 6

    Asincronia: Future, async, await e gestione degli errori

    Un'app deve leggere dati dalla rete o dal disco senza bloccare l'interfaccia. In Dart le operazioni che finiranno "più tardi" restituiscono un Future<T>: una promessa di valore.

    Regole pratiche:

    • una funzione che usa await deve essere marcata async;
    • una funzione async restituisce sempre un Future (usa Future<void> se non ritorna nulla);
    • await mette in pausa solo quella funzione, non l'intera app;
    • gli errori si gestiscono con try / catch / finally;
    • per operazioni indipendenti in parallelo usa Future.wait([...]).

    Esiste anche lo Stream<T>: una sequenza di valori nel tempo (es. la posizione GPS), che si consuma con await for o listen.

    Errore tipico del principiante: dimenticare await e stampare Instance of 'Future<String>' invece del valore.

    // Simula una chiamata di rete che impiega 2 secondi
    Future<String> caricaUtente(int id) async {
      await Future.delayed(const Duration(seconds: 2));
      if (id <= 0) {
        throw Exception('ID non valido: $id');
      }
      return 'Utente #$id';
    }
    
    Future<List<String>> caricaPost() async {
      await Future.delayed(const Duration(seconds: 1));
      return ['Primo post', 'Secondo post'];
    }
    
    Future<void> main() async {
      print('Inizio caricamento...');
    
      try {
        final utente = await caricaUtente(7);
        print(utente);
    
        // Due operazioni in parallelo: dura quanto la più lenta
        final risultati = await Future.wait([
          caricaUtente(8),
          caricaPost(),
        ]);
        print(risultati);
    
        await caricaUtente(-1); // genera un errore
      } catch (e) {
        print('Errore catturato: $e');
      } finally {
        print('Operazione terminata');
      }
    
      // Senza await ottieni il Future, non il valore
      print(caricaUtente(1)); // Instance of 'Future<String>'
    }

    Risultato atteso

    Dopo circa 2 secondi appare "Utente #7", poi la lista dei risultati paralleli, quindi "Errore catturato: Exception: ID non valido: -1", "Operazione terminata" e infine "Instance of 'Future<String>'".

  7. 7

    Mettere tutto insieme in un widget Flutter

    Ora colleghiamo i puntini: una piccola app Flutter che usa una classe modello, una funzione asincrona, una lista con map e la null safety.

    Cosa riconoscerai:

    • class HomePage extends StatefulWidget → ereditarietà e @override
    • Future<void> _carica() async → asincronia
    • List<Prodotto>? _prodotti → variabile nullable usata come stato "non ancora caricato"
    • _prodotti!.map((p) => ListTile(...)).toList() → metodi delle collezioni
    • parametri nominati ovunque nei costruttori dei widget

    Crea un progetto con flutter create demo_dart, incolla il codice in lib/main.dart ed esegui con flutter run.

    import 'package:flutter/material.dart';
    
    class Prodotto {
      final String nome;
      final double prezzo;
      const Prodotto({required this.nome, required this.prezzo});
    }
    
    Future<List<Prodotto>> caricaProdotti() async {
      await Future.delayed(const Duration(seconds: 2));
      return const [
        Prodotto(nome: 'Tastiera', prezzo: 49.90),
        Prodotto(nome: 'Mouse', prezzo: 29.90),
        Prodotto(nome: 'Monitor', prezzo: 199.00),
      ];
    }
    
    void main() => runApp(const MyApp());
    
    class MyApp extends StatelessWidget {
      const MyApp({super.key});
    
      @override
      Widget build(BuildContext context) {
        return MaterialApp(
          title: 'Dart essenziale',
          theme: ThemeData(colorSchemeSeed: Colors.indigo, useMaterial3: true),
          home: const HomePage(),
        );
      }
    }
    
    class HomePage extends StatefulWidget {
      const HomePage({super.key});
    
      @override
      State<HomePage> createState() => _HomePageState();
    }
    
    class _HomePageState extends State<HomePage> {
      List<Prodotto>? _prodotti; // null = ancora in caricamento
      String? _errore;
    
      @override
      void initState() {
        super.initState();
        _carica();
      }
    
      Future<void> _carica() async {
        try {
          final dati = await caricaProdotti();
          if (!mounted) return;
          setState(() => _prodotti = dati);
        } catch (e) {
          if (!mounted) return;
          setState(() => _errore = e.toString());
        }
      }
    
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          appBar: AppBar(title: const Text('Catalogo')),
          body: _errore != null
              ? Center(child: Text('Errore: $_errore'))
              : _prodotti == null
                  ? const Center(child: CircularProgressIndicator())
                  : ListView(
                      children: _prodotti!
                          .map((p) => ListTile(
                                leading: const Icon(Icons.shopping_bag),
                                title: Text(p.nome),
                                trailing:
                                    Text('${p.prezzo.toStringAsFixed(2)} EUR'),
                              ))
                          .toList(),
                    ),
        );
      }
    }

    Risultato atteso

    All'avvio vedi uno spinner per 2 secondi, poi la lista dei tre prodotti con nome e prezzo. Se in caricaProdotti lanci un'eccezione, compare invece il messaggio di errore.

CondividiXLinkedInFacebookWhatsApp

Commenti (0)

Ancora nessun commento. Inizia tu!