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:
- Introduzione a Flutter e tre demo standard (questo articolo).
- Prima app: la testa di Cheap scrive CIAO | HELLO (pubblicazione in arrivo).
- Seconda app: CIAO e HELLO via BLE con l’ESP32-C6 (pubblicazione in arrivo).
- 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.

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 metodobuild()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()chiamarunApp()con il widget radice.MaterialAppimposta tema, titolo e pagina iniziale con lo stile Material 3;colorSchemeSeedgenera tutta la palette partendo da un solo colore, il turchese del sito.- La lista
demousa i record di Dart 3:(String, IconData, Widget)è una piccola tupla con titolo, icona e pagina. Nel ciclofordentro 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è unStatefulWidget: il widget in sé non cambia, mentre lo stato vive nella classe_DemoContatoreState, che Flutter conserva tra un ridisegno e l’altro.incrementa()aumentaconteggiodentrosetState(): è il segnale per Flutter che deve richiamarebuild().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 campifinal: i dati restano separati dalla grafica.ListView.buildercostruisce le righe solo quando servono, mentre scorri. Con 4 schede non cambia niente, ma con 4.000 righe fa la differenza.DettaglioSchedariceve la scheda nel costruttore: passare i dati da una pagina all’altra è semplice come chiamare un costruttore.- Le parole
constdicono 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 induration, con la curvaCurves.easeOut. Non serve gestire fotogrammi o controller.GestureDetectorrende “toccabile” qualsiasi widget.- Il
semanticLabeldell’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

Con l’app aperta, nel terminale premi r per l’hot reload, R per l’hot restart e q per uscire.
Problemi comuni
flutternon è riconosciuto come comando: mancaC:\flutter\binnel 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-licensese 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
Columno di unaRowè più grande dello spazio. Si sistema conFlexible,Expandedo 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.
