Struttura di base di un’app Flutter
Obiettivo
Capire e sperimentare la struttura fondamentale di una app Flutter utilizzando i widget MaterialApp, Scaffold, AppBar, NavigationBar, FloatingActionButton e Drawer.
1. MaterialApp
Descrizione
MaterialApp è il widget principale che configura l’intera app in stile Material Design. Offre gestione del tema, routing, localizzazione, transizioni, e molto altro. È la base per creare app moderne e coerenti con le linee guida di Google.
Gestione del tema
MaterialApp permette di definire:
- un
theme(chiaro) - un
darkTheme(scuro) - un
themeMode(per scegliere quale usare)
Puoi anche usare ColorScheme.fromSeed per generare automaticamente una palette Material 3 da un solo colore base.
Esempio classico (Material 2):
MaterialApp(
title: 'La mia prima app',
theme: ThemeData(primarySwatch: Colors.blue),
home: MyHomePage(),
debugShowCheckedModeBanner: false,
)Esempio moderno (Material 3 con seed):
MaterialApp(
title: 'App con tema generato',
theme: ThemeData.from(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.teal),
useMaterial3: true,
),
darkTheme: ThemeData.from(
colorScheme: ColorScheme.fromSeed(
seedColor: Colors.teal,
brightness: Brightness.dark,
),
useMaterial3: true,
),
themeMode: ThemeMode.system,
home: MyHomePage(),
)Attributi utili
title: Titolo visibile nel task switcher.theme: Tema principale dell’app (colori, stili).darkTheme: Tema per modalità scura.themeMode: Selettore tra chiaro, scuro o automatico.home: Widget di partenza dell’app.routes: Mappa delle rotte nominate.initialRoute: Rotta iniziale.debugShowCheckedModeBanner: Mostra o nasconde il banner DEBUG.
Alternative a MaterialApp
Anche se MaterialApp è lo standard per creare applicazioni Flutter in stile Material Design, non è l’unica opzione disponibile. Flutter offre altre strutture base per casi d’uso diversi:
1. CupertinoApp
Usata per creare app in stile iOS, con supporto a widget Cupertino.
CupertinoApp(
home: CupertinoPageScaffold(
navigationBar: CupertinoNavigationBar(
middle: Text('Titolo iOS'),
),
child: Center(child: Text('Contenuto')),
),
)2. WidgetsApp
È la base comune tra MaterialApp e CupertinoApp. Non fornisce widget grafici predefiniti, ma gestisce routing, localizzazione e navigazione.
WidgetsApp(
color: Colors.white,
builder: (context, child) => MyCustomApp(child: child),
home: MyHomePage(),
)3. App minimal senza struttura predefinita
Per esperimenti, librerie o ambienti altamente personalizzati:
void main() {
runApp(
Directionality(
textDirection: TextDirection.ltr,
child: Center(child: Text('App base senza MaterialApp')),
),
);
}App multipiattaforma: approccio consigliato
Per creare un’app Flutter multipiattaforma (Android e iOS) coerente con il sistema operativo, ci sono due approcci principali:
Approccio MaterialApp + widget adattivi
- Usa
MaterialAppconuseMaterial3: true - Usa widget adattivi come
Switch.adaptive,Scrollbar.adaptive, ecc. - Personalizza il layout o stile usando
Platform.isIOSdove serve
Esempio:
MaterialApp(
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.indigo),
useMaterial3: true,
),
home: Platform.isIOS ? CupertinoStyledPage() : MaterialStyledPage(),
)Confronto rapido degli approcci
| Obiettivo | Miglior approccio |
|---|---|
| App coerente su entrambe le piattaforme | MaterialApp con Material 3 + widget adattivi |
| App con stile nativo per ogni OS | logica Platform.isIOS |
| Sviluppo rapido e codice unificato | MaterialApp + layout responsivo |
Quando usare un’alternativa
- Vuoi replicare lo stile nativo iOS → usa
CupertinoApp - Vuoi una base pulita e neutra per widget personalizzati → usa
WidgetsApp - Stai creando un widget standalone o una libreria → usa solo ciò che serve, senza Material
Attributi utili
title: Titolo visibile nel task switcher.theme: Tema principale dell’app (colori, stili).home: Widget di partenza dell’app.routes: Mappa delle rotte nominate.initialRoute: Rotta iniziale.debugShowCheckedModeBanner: Mostra o nasconde il banner DEBUG.
MaterialApp(
title: 'La mia prima app',
theme: ThemeData(primarySwatch: Colors.blue),
home: MyHomePage(),
debugShowCheckedModeBanner: false,
)2. Scaffold
Descrizione
Scaffold è uno dei widget più importanti in Flutter perché fornisce la struttura visiva e logica di una schermata. Viene usato come contenitore principale di ogni “pagina” o vista dell’app. Integra automaticamente molti altri widget chiave del Material Design, come:
AppBar: la barra superioreDrawer: il menu lateraleFloatingActionButton: pulsante d’azione principaleBottomNavigationBar: barra di navigazione in bassoSnackBar,BottomSheet,FAB, ecc.
Scaffold si adatta bene al contesto (es. mostra correttamente il Drawer o il FAB) e gestisce automaticamente spazi come lo SafeArea, il rientro sotto tastiere o notch.
Attributi utili
appBar: UnaAppBarda visualizzare in alto.body: Contenuto principale della schermata.drawer: Menu laterale a scomparsa.bottomNavigationBar: Barra inferiore di navigazione, supportaBottomNavigationBareNavigationBar.floatingActionButton: Pulsante d’azione fluttuante.backgroundColor: Colore dello sfondo.
Esempio
Scaffold(
appBar: AppBar(title: Text('Home')),
body: Center(child: Text('Contenuto principale')),
bottomNavigationBar: NavigationBar(
selectedIndex: 0,
onDestinationSelected: (index) => print('Selezionato: \$index'),
destinations: [
NavigationDestination(icon: Icon(Icons.home), label: 'Home'),
NavigationDestination(icon: Icon(Icons.settings), label: 'Impostazioni'),
],
),
floatingActionButton: FloatingActionButton(
onPressed: () {},
child: Icon(Icons.add),
),
)3. AppBar
Descrizione
AppBar è una barra nella parte superiore dello schermo che può contenere un titolo, pulsanti di azione, icone e menu.
Attributi utili
title: Un widget, tipicamenteText, che rappresenta il titolo.leading: Icona o widget visualizzato a sinistra (es. hamburger menu).actions: Lista di widget (tipicamenteIconButton) visualizzati a destra.backgroundColor: Colore dello sfondo.elevation: Ombra sotto l’app bar.centerTitle: Per centrare il title true/false.
Esempio
AppBar(
title: Text('Titolo'),
actions: [
IconButton(icon: Icon(Icons.search), onPressed: () {})
],
)4. BottomNavigationBar (NavigationBar)
Descrizione
BottomNavigationBar consente la navigazione tra diverse schermate principali di un’app. Si trova in basso e visualizza icone e testi.
Attributi utili
items: Lista di elementi (BottomNavigationBarItem).currentIndex: Indice attualmente selezionato.onTap: Funzione callback al tap su un item.type: Tipo di visualizzazione (fixedoshifting).selectedItemColor: Colore dell’elemento selezionato.
Esempio
BottomNavigationBar(
currentIndex: 0,
onTap: (index) => print('Hai premuto la voce $index'),
items: [
BottomNavigationBarItem(icon: Icon(Icons.home), label: 'Home'),
BottomNavigationBarItem(icon: Icon(Icons.settings), label: 'Impostazioni'),
],
)5. FloatingActionButton
Descrizione
Un bottone rotondo che “fluttua” sopra il layout. Usato spesso per azioni principali (es. aggiungere, scrivere, creare).
Attributi utili
onPressed: Funzione da eseguire al tap.child: Contenuto interno (di solitoIcon).backgroundColor: Colore di sfondo.tooltip: Testo mostrato come tooltip.heroTag: Per transizioni animate.
Esempio
FloatingActionButton(
onPressed: () => print('Premuto'),
child: Icon(Icons.add),
tooltip: 'Aggiungi',
)6. Drawer
Descrizione
Drawer è un menu laterale che appare scorrendo o toccando l’icona del menu. Utile per la navigazione secondaria o opzioni generali.
Attributi utili
child: Contenuto del Drawer (tipicamenteListView).elevation: Altezza dell’ombra.backgroundColor: Colore dello sfondo.width: Larghezza del menu.
Esempio
Drawer(
child: ListView(// o a Column
children: [
DrawerHeader(child: Text('Menu')),
ListTile(title: Text('Home'), onTap: () {}),
ListTile(title: Text('Profilo'), onTap: () {}),
],
),
)7. PageView
Descrizione
Un PageView consente di scorrere tra una serie di widget come se fossero pagine.
È utile per creare interfacce utente in stile onboarding, gallerie di immagini o contenuti multipli in sequenza.
Attributi utili
controller: Gestisce il controllo del movimento tra le pagine.children: Un elenco di widget che compongono le pagine.scrollDirection: Direzione dello scorrimento (orizzontale o verticale).onPageChanged: Callback che viene chiamato quando la pagina cambia.
Esempio
import 'package:flutter/material.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: PageViewExample(),
);
}
}
class PageViewExample extends StatelessWidget {
final PageController _controller = PageController();
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('PageView Example'),
),
body: PageView(
controller: _controller,
children: [
Container(color: Colors.red, child: Center(child: Text('Pagina 1'))),
Container(color: Colors.green, child: Center(child: Text('Pagina 2'))),
Container(color: Colors.blue, child: Center(child: Text('Pagina 3'))),
],
),
);
}
}Esempio - Onboarding App
import 'package:flutter/material.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: OnboardingPage(),
);
}
}
class OnboardingPage extends StatefulWidget {
@override
_OnboardingPageState createState() => _OnboardingPageState();
}
class _OnboardingPageState extends State<OnboardingPage> {
final PageController _pageController = PageController();
int _currentPage = 0;
void _nextPage() {
if (_currentPage < 2) {
_pageController.nextPage(
duration: Duration(milliseconds: 300),
curve: Curves.easeInOut,
);
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: Stack(
children: [
PageView(
controller: _pageController,
onPageChanged: (index) {
setState(() {
_currentPage = index;
});
},
children: [
_buildPage(
color: Colors.red,
text: 'Benvenuto in MyApp!',
image: Icons.ac_unit,
),
_buildPage(
color: Colors.green,
text: 'Organizza le tue attività.',
image: Icons.assignment,
),
_buildPage(
color: Colors.blue,
text: 'Inizia ora!',
image: Icons.arrow_forward,
),
],
),
Positioned(
bottom: 20,
left: 0,
right: 0,
child: Center(
child: ElevatedButton(
onPressed: _nextPage,
child: Text(_currentPage == 2 ? 'Inizia' : 'Avanti'),
),
),
),
],
),
);
}
Widget _buildPage({required Color color, required String text, required IconData image}) {
return Container(
color: color,
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(image, size: 100, color: Colors.white),
SizedBox(height: 20),
Text(
text,
style: TextStyle(fontSize: 24, color: Colors.white),
),
],
),
),
);
}
}È possibile utilizzare PageController per controllare il movimento tra le pagine e monitorare l’indice corrente.
8. Snackbar
Descrizione
Una Snackbar è un piccolo messaggio temporaneo che appare nella parte inferiore dello schermo.
Serve per fornire feedback immediato all’utente dopo un’azione, come l’invio di un form o la rimozione di un elemento.
Attributi utili
content: Il widget (tipicamente unText) mostrato nella snackbar.duration: Durata della visualizzazione.action: Aggiunge un bottone (es. “Annulla”).backgroundColor: Colore di sfondo della snackbar.behavior: Controlla la posizione (es.SnackBarBehavior.floating).
Esempio
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Elemento salvato'),
duration: Duration(seconds: 2),
action: SnackBarAction(
label: 'Annulla',
onPressed: () {
// codice per annullare
},
),
),
);A partire da Flutter 2.0, ScaffoldMessenger.of(context) è il modo corretto per mostrare una snackbar, al posto di Scaffold.of(context).
9. Dialog
Descrizione
showDialog è una funzione che apre un modale centrato sopra il contenuto corrente.
È utile per conferme, avvisi, messaggi brevi o azioni rapide, senza lasciare la schermata attuale.
Attributi utili di showDialog
context: Contesto da cui viene chiamato.builder: Funzione che restituisce il widget da mostrare (tipicamenteAlertDialogoDialog).barrierDismissible: Setrue, l’utente può chiudere il dialog toccando fuori dal box (default:true).barrierColor: Colore dell’overlay dietro il dialog.useSafeArea: Setrue, evita aree protette (notch, status bar).useRootNavigator: Setrue, usa ilNavigatordi livello più alto.
Variante base: AlertDialog
showDialog(
context: context,
builder: (context) {
return AlertDialog(
title: Text('Attenzione'),
content: Text('Sei sicuro di voler continuare?'),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: Text('Annulla'),
),
TextButton(
onPressed: () {
// azione confermata
Navigator.pop(context);
},
child: Text('Conferma'),
),
],
);
},
);Variante personalizzata: Dialog
showDialog(
context: context,
builder: (_) => Dialog(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20),
),
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text('Messaggio personalizzato'),
SizedBox(height: 20),
ElevatedButton(
onPressed: () => Navigator.pop(context),
child: Text('Chiudi'),
),
],
),
),
),
);Variante con SimpleDialog
showDialog(
context: context,
builder: (context) {
return SimpleDialog(
title: Text('Seleziona un’opzione'),
children: [
SimpleDialogOption(
onPressed: () => Navigator.pop(context, 'A'),
child: Text('Opzione A'),
),
SimpleDialogOption(
onPressed: () => Navigator.pop(context, 'B'),
child: Text('Opzione B'),
),
],
);
},
);Ricevere un valore dal dialog
final risultato = await showDialog<String>(
context: context,
builder: (_) => AlertDialog(
content: Text('Scegli un’opzione'),
actions: [
TextButton(
onPressed: () => Navigator.pop(context, 'Accetta'),
child: Text('Accetta'),
),
TextButton(
onPressed: () => Navigator.pop(context, 'Rifiuta'),
child: Text('Rifiuta'),
),
],
),
);
print('Hai scelto: $risultato');Altre varianti utili
| Variante | Descrizione |
|---|---|
AlertDialog | Layout standard con titolo, testo e azioni |
SimpleDialog | Per selezione rapida tra più opzioni |
Dialog | Totalmente personalizzabile |
showGeneralDialog | Per creare dialog con animazioni personalizzate |
I dialog sono modali e vanno chiusi sempre con Navigator.pop(context), eventualmente passando un valore di ritorno.
Lo incontreremo nuovamente durante la lezione sulla navigazione
Esercizio Pratico
Prima prova pratica, avrete la possibilità di applicare tutto ciò che fino a ora abbiamo esplorato. L’esercitazione si baserà nello sviluppare un applicazione a singola pagina che rappresenti la pagina di un prodotto all’interno di un E-commerce.