Flutter da zero: cos’è, come si installa e tre demo standard

Flutter da zero: cos’è, come si installa e tre demo standard

Cheap, la mascotte del sito, ha un volto fatto di LED e un sacco di cose da dire. Gli manca solo un’app in tasca per parlarci. Comprarne una già fatta? Figuriamoci: con 2 centesimi di budget se la scrive da solo. In questa serie di quattro articoli impariamo Flutter partendo da zero e arriviamo a un’app che parla via Bluetooth Low Energy con un ESP32-C6. In questo primo articolo vediamo cos’è Flutter, come si installa e tre demo standard che contengono già quasi tutto quello che serve.

La serie completa:

  1. Introduzione a Flutter e tre demo standard (questo articolo).
  2. Prima app: la testa di Cheap scrive CIAO | HELLO (pubblicazione in arrivo).
  3. Seconda app: CIAO e HELLO via BLE con l’ESP32-C6 (pubblicazione in arrivo).
  4. Terza app: il pulsante dell’ESP32-C6 accende la scritta, l’app fa lampeggiare il LED (pubblicazione in arrivo).

Cos’è Flutter

Flutter è un framework open source di Google (licenza BSD) per costruire interfacce grafiche. Scrivi il codice una volta, in linguaggio Dart, e lo stesso progetto diventa un’app per Android, iOS, web, Windows, macOS e Linux.

La differenza rispetto ad altri strumenti multipiattaforma è che Flutter non usa i pulsanti e le liste del sistema operativo: disegna da solo ogni pixel con il suo motore di rendering (Impeller, scritto in C++). Per questo un’app Flutter è identica su un telefono Android e su un iPhone, ed è veloce: il motore lavora direttamente con la GPU.

Quando serve qualcosa che solo il sistema operativo sa fare, come il Bluetooth, i sensori o i file, Flutter usa dei plugin: pacchetti con una parte in Dart e una parte nativa per ogni piattaforma. Nella terza e nella quarta app useremo il plugin universal_ble.

Architettura di Flutter: codice Dart, framework, motore di rendering, piattaforme e plugin
Dal codice Dart all’app: il framework e il motore sono gli stessi ovunque, cambia solo il pacchetto finale.

Tutto è un widget

In Flutter l’interfaccia è un albero di widget: un testo è un widget, un pulsante è un widget, il margine attorno al pulsante è un widget, la pagina intera è un widget. Ne esistono di due tipi:

  • StatelessWidget: non cambia mai da solo. Riceve dei dati e li disegna. Esempio: un’etichetta con il nome di una scheda.
  • StatefulWidget: ha uno stato, cioè dei dati che cambiano nel tempo, come un contatore o un LED acceso o spento. Quando lo stato cambia chiami setState() e Flutter richiama il metodo build() per ridisegnare.

Il metodo build() descrive come deve apparire il widget adesso. Non scrivi “sposta questo, nascondi quello”: descrivi il risultato e Flutter calcola le differenze. È lo stesso approccio “dichiarativo” di React o di Jetpack Compose.

Hot reload

Il motivo per cui ci si affeziona a Flutter: con l’app in esecuzione modifichi il codice, salvi, e in meno di un secondo vedi la modifica sul telefono senza perdere lo stato. Se il contatore era a 7, resta a 7 con il nuovo colore. Per le modifiche più profonde (per esempio dentro main() o ai valori iniziali dello stato) c’è l’hot restart, che riavvia l’app in un paio di secondi.

Flutter o altro?

Strumento Linguaggio Piattaforme Quando sceglierlo
Flutter Dart Android, iOS, web, desktop Un solo codice, interfaccia curata e identica ovunque: la scelta di questa serie
Kotlin + Jetpack Compose Kotlin Android App solo Android con integrazione profonda nel sistema
React Native JavaScript / TypeScript Android, iOS Se il team arriva dal web e conosce già React
MIT App Inventor Blocchi grafici Android, iOS Prototipi lampo e didattica, con personalizzazione limitata

Cosa serve

Componente Esempio Note
PC Windows 10/11, macOS o Linux Gli esempi sono stati preparati su Windows 11
Flutter SDK 3.47.6, canale stable Contiene già Dart 3.13.5
Editor Visual Studio Code con l’estensione Flutter Va bene anche Android Studio
Per provare su Android Android Studio (SDK ed emulatore) e JDK 17 o successivo Il Gradle di Flutter 3.47 non parte con Java 11
Per provare su iPhone Un Mac con Xcode Senza Mac puoi usare Android, il browser o Windows

Installazione

Su Windows il modo più semplice è clonare il canale stable di Flutter in una cartella senza spazi nel percorso, aggiungere C:\flutter\bin al PATH e lasciare che flutter doctor controlli il resto:

git clone --depth 1 -b stable https://github.com/flutter/flutter.git C:\flutter
flutter --version
flutter doctor

Al primo avvio Flutter scarica il proprio SDK Dart e il motore per Windows. flutter doctor mostra una riga per ogni piattaforma: per questo primo articolo bastano Chrome o Visual Studio (per le app Windows). Per Android servono Android Studio, i “command-line tools” dell’SDK e l’accettazione delle licenze con flutter doctor --android-licenses.

Il progetto delle demo

flutter create --org com.webappportal demo_standard
cd demo_standard

Il comando crea un progetto completo e funzionante. Le cartelle che contano:

  • lib/: il codice Dart dell’app. Il punto di partenza è lib/main.dart.
  • test/: i test automatici.
  • pubspec.yaml: nome, versione e dipendenze del progetto (i pacchetti di pub.dev).
  • android/, ios/, web/, windows/…: i “contenitori” nativi per ogni piattaforma. Li tocchi raramente, per esempio per i permessi del Bluetooth.

Mettiamo le tre demo in tre file separati e usiamo main.dart come menu.

main.dart: il menu

import 'package:flutter/material.dart';

import 'demo1_contatore.dart';
import 'demo2_lista.dart';
import 'demo3_animazione.dart';

void main() => runApp(const DemoApp());

class DemoApp extends StatelessWidget {
  const DemoApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Tre demo Flutter',
      debugShowCheckedModeBanner: false,
      theme: ThemeData(colorSchemeSeed: const Color(0xFF2DD4BF)),
      home: const Menu(),
    );
  }
}

class Menu extends StatelessWidget {
  const Menu({super.key});

  @override
  Widget build(BuildContext context) {
    final demo = <(String, IconData, Widget)>[
      ('1. Contatore', Icons.add_circle_outline, const DemoContatore()),
      ('2. Lista e dettaglio', Icons.list, const DemoLista()),
      ('3. Animazione', Icons.lightbulb_outline, const DemoAnimazione()),
    ];
    return Scaffold(
      appBar: AppBar(title: const Text('Tre demo Flutter')),
      body: ListView(
        children: [
          for (final (titolo, icona, pagina) in demo)
            ListTile(
              leading: Icon(icona),
              title: Text(titolo),
              trailing: const Icon(Icons.chevron_right),
              onTap: () =>
                  Navigator.of(context)
                      .push(MaterialPageRoute(builder: (_) => pagina)),
            ),
        ],
      ),
    );
  }
}

Cosa succede:

  • main() chiama runApp() con il widget radice. MaterialApp imposta tema, titolo e pagina iniziale con lo stile Material 3; colorSchemeSeed genera tutta la palette partendo da un solo colore, il turchese del sito.
  • La lista demo usa i record di Dart 3: (String, IconData, Widget) è una piccola tupla con titolo, icona e pagina. Nel ciclo for dentro la lista dei figli la “smontiamo” direttamente in tre variabili.
  • Navigator.of(context).push() apre una nuova pagina sopra quella attuale. La freccia “indietro” nella barra in alto la aggiunge Flutter da solo.

Demo 1: il contatore

È la stessa idea dell’app che flutter create genera da sola, tradotta e ridotta all’osso. È il “ciao mondo” di Flutter perché contiene il concetto più importante: lo stato.

import 'package:flutter/material.dart';

/// Demo 1: il contatore, la stessa idea dell'app creata da "flutter create".
/// Un widget con stato: ogni tocco cambia un numero e Flutter ridisegna.
class DemoContatore extends StatefulWidget {
  const DemoContatore({super.key});

  @override
  State<DemoContatore> createState() => _DemoContatoreState();
}

class _DemoContatoreState extends State<DemoContatore> {
  int conteggio = 0;

  void incrementa() => setState(() => conteggio++);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Contatore')),
      body: Center(
        child: Column(
          mainAxisSize: MainAxisSize.min,
          children: [
            const Text('Hai premuto il pulsante:'),
            Text(
              '$conteggio',
              style: Theme.of(context).textTheme.displayMedium,
            ),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: incrementa,
        tooltip: 'Aggiungi uno',
        child: const Icon(Icons.add),
      ),
    );
  }
}
  • DemoContatore è un StatefulWidget: il widget in sé non cambia, mentre lo stato vive nella classe _DemoContatoreState, che Flutter conserva tra un ridisegno e l’altro.
  • incrementa() aumenta conteggio dentro setState(): è il segnale per Flutter che deve richiamare build().
  • Scaffold è lo scheletro di una pagina Material: barra in alto, corpo, pulsante flottante.

Prova l’hot reload: con l’app aperta cambia il testo 'Hai premuto il pulsante:', salva e guarda il conteggio. Resta dov’era.

Demo 2: lista e dettaglio

Il secondo schema che troverai in quasi ogni app: una lista di elementi e una pagina di dettaglio per ciascuno.

import 'package:flutter/material.dart';

/// Demo 2: una lista che apre una pagina di dettaglio (navigazione).
class Scheda {
  const Scheda(this.nome, this.descrizione);
  final String nome;
  final String descrizione;
}

const schede = [
  Scheda('ESP32-S3', 'Wi-Fi e BLE, due core: la scheda dei tutorial avanzati.'),
  Scheda('ESP32-C6', 'Wi-Fi 6, BLE 5 e 802.15.4: la useremo con Cheap.'),
  Scheda('ESP32-P4', 'Il più potente: video, display e fotocamera.'),
  Scheda('nRF5340', 'Nordic, due core Arm e Bluetooth LE Audio.'),
];

class DemoLista extends StatelessWidget {
  const DemoLista({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Schede')),
      body: ListView.builder(
        itemCount: schede.length,
        itemBuilder: (context, i) => ListTile(
          leading: const Icon(Icons.memory),
          title: Text(schede[i].nome),
          onTap: () => Navigator.of(
            context,
          ).push(MaterialPageRoute(builder: (_) => DettaglioScheda(schede[i]))),
        ),
      ),
    );
  }
}

class DettaglioScheda extends StatelessWidget {
  const DettaglioScheda(this.scheda, {super.key});
  final Scheda scheda;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text(scheda.nome)),
      body: Padding(
        padding: const EdgeInsets.all(24),
        child: Text(
          scheda.descrizione,
          style: Theme.of(context).textTheme.titleLarge,
        ),
      ),
    );
  }
}
  • Scheda è una normale classe Dart con due campi final: i dati restano separati dalla grafica.
  • ListView.builder costruisce le righe solo quando servono, mentre scorri. Con 4 schede non cambia niente, ma con 4.000 righe fa la differenza.
  • DettaglioScheda riceve la scheda nel costruttore: passare i dati da una pagina all’altra è semplice come chiamare un costruttore.
  • Le parole const dicono a Flutter che quel widget non cambierà mai: lo crea una volta sola e lo riusa.

Demo 3: un’animazione implicita

Il terzo ingrediente: il movimento. Un LED virtuale che al tocco si accende, cresce e si illumina.

import 'package:flutter/material.dart';

/// Demo 3: un'animazione implicita. Cambi i valori e AnimatedContainer
/// passa da uno stato all'altro da solo, senza scrivere fotogramma per fotogramma.
class DemoAnimazione extends StatefulWidget {
  const DemoAnimazione({super.key});

  @override
  State<DemoAnimazione> createState() => _DemoAnimazioneState();
}

class _DemoAnimazioneState extends State<DemoAnimazione> {
  bool acceso = false;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Animazione')),
      body: Center(
        child: GestureDetector(
          onTap: () => setState(() => acceso = !acceso),
          child: AnimatedContainer(
            duration: const Duration(milliseconds: 400),
            curve: Curves.easeOut,
            width: acceso ? 160 : 100,
            height: acceso ? 160 : 100,
            decoration: BoxDecoration(
              shape: BoxShape.circle,
              color: acceso ? const Color(0xFFF0A04B) : Colors.grey.shade400,
              boxShadow: [
                if (acceso)
                  const BoxShadow(
                    color: Color(0xAAF0A04B),
                    blurRadius: 40,
                    spreadRadius: 10,
                  ),
              ],
            ),
            child: Icon(
              acceso ? Icons.lightbulb : Icons.lightbulb_outline,
              size: 48,
              semanticLabel: acceso ? 'LED acceso' : 'LED spento',
            ),
          ),
        ),
      ),
    );
  }
}
  • AnimatedContainer è un’animazione implicita: quando cambi larghezza, colore o ombra, passa dal vecchio al nuovo valore nel tempo indicato in duration, con la curva Curves.easeOut. Non serve gestire fotogrammi o controller.
  • GestureDetector rende “toccabile” qualsiasi widget.
  • Il semanticLabel dell’icona serve ai lettori di schermo e, come vedremo subito, anche ai test.

I test

Flutter ha un sistema di test integrato che fa girare i widget senza telefono e senza finestra: tocca i pulsanti, aspetta le animazioni e controlla cosa c’è sullo schermo. Lo useremo in tutta la serie, anche per simulare l’ESP32-C6.

import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';

import 'package:demo_standard/main.dart';

void main() {
  testWidgets('Demo 1: il contatore sale', (tester) async {
    await tester.pumpWidget(const DemoApp());
    await tester.tap(find.text('1. Contatore'));
    await tester.pumpAndSettle();
    expect(find.text('0'), findsOneWidget);
    await tester.tap(find.byIcon(Icons.add));
    await tester.pump();
    expect(find.text('1'), findsOneWidget);
  });

  testWidgets('Demo 2: dalla lista al dettaglio', (tester) async {
    await tester.pumpWidget(const DemoApp());
    await tester.tap(find.text('2. Lista e dettaglio'));
    await tester.pumpAndSettle();
    await tester.tap(find.text('ESP32-C6'));
    await tester.pumpAndSettle();
    expect(find.textContaining('la useremo con Cheap'), findsOneWidget);
  });

  testWidgets('Demo 3: il LED si accende', (tester) async {
    await tester.pumpWidget(const DemoApp());
    await tester.tap(find.text('3. Animazione'));
    await tester.pumpAndSettle();
    await tester.tap(find.byIcon(Icons.lightbulb_outline));
    await tester.pumpAndSettle();
    expect(find.byIcon(Icons.lightbulb), findsOneWidget);
  });
}

pumpWidget() monta l’app, tap() simula un tocco, pump() disegna un fotogramma e pumpAndSettle() aspetta che le animazioni finiscano. Si lanciano con:

flutter analyze
flutter test
Analyzing demo_standard...
No issues found!
00:01 +3: All tests passed!

Prova e risultati attesi

Scegli dove far girare l’app. flutter devices elenca i dispositivi disponibili:

flutter run -d chrome     # nel browser
flutter run -d windows    # come app di Windows
flutter run               # sul telefono Android collegato via USB, con il debug USB attivo
Le tre demo Flutter: menu, contatore, dettaglio della lista e LED animato
Il menu e le tre demo: il contatore dopo tre tocchi, il dettaglio dell’ESP32-C6 e il LED acceso.

Con l’app aperta, nel terminale premi r per l’hot reload, R per l’hot restart e q per uscire.

Problemi comuni

  • flutter non è riconosciuto come comando: manca C:\flutter\bin nel PATH, oppure il terminale era già aperto prima della modifica. Chiudilo e riaprilo.
  • “Gradle requires JVM 17 or later”: Gradle sta usando un Java troppo vecchio. Installa un JDK 17 o successivo e indicalo a Flutter con flutter config --jdk-dir="percorso del JDK".
  • “Android license status unknown”: lancia flutter doctor --android-licenses e accetta le licenze dell’SDK.
  • La modifica non si vede con l’hot reload: hai cambiato main() o il valore iniziale di un campo dello stato. Usa l’hot restart (R).
  • “A RenderFlex overflowed by … pixels”: il contenuto di una Column o di una Row è più grande dello spazio. Si sistema con Flexible, Expanded o una lista scorrevole. Ci capiterà nel prossimo articolo.

Va oltre

  • Nel prossimo articolo disegniamo la testa di Cheap con il suo volto a matrice di LED RGB e le facciamo scrivere CIAO | HELLO (pubblicazione in arrivo).
  • La documentazione ufficiale, con molti esempi, è su docs.flutter.dev.
  • Altre guide sulle app per i nostri dispositivi nella sezione App Mobile.

Imprenditore, maker, MBSE enthusiast e co-autore di Web App Portal. Collegati su LinkedIn.