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
MaterialApp
conuseMaterial3: true
- Usa widget adattivi come
Switch.adaptive
,Scrollbar.adaptive
, ecc. - Personalizza il layout o stile usando
Platform.isIOS
dove 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
: UnaAppBar
da visualizzare in alto.body
: Contenuto principale della schermata.drawer
: Menu laterale a scomparsa.bottomNavigationBar
: Barra inferiore di navigazione, supportaBottomNavigationBar
eNavigationBar
.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 (fixed
oshifting
).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 (tipicamenteAlertDialog
oDialog
).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 ilNavigator
di 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.