Sei sulla pagina 1di 508

Sviluppare applicazioni con Flutter

Guida al framework e al linguaggio Dart per


lo sviluppo mobile cross platform

Vincenzo Giacchina
© Apogeo - IF - Idee editoriali Feltrinelli s.r.l.
Socio Unico Giangiacomo Feltrinelli Editore s.r.l.

ISBN ebook: 9788850319756

Il presente file può essere usato esclusivamente per finalità di


carattere personale. Tutti i contenuti sono protetti dalla Legge sul
diritto d’autore.

Nomi e marchi citati nel testo sono generalmente depositati o


registrati dalle rispettive case produttrici.

L’edizione cartacea è in vendita nelle migliori librerie.

Sito web: www.apogeonline.com

Scopri le novità di Apogeo su Facebook

Seguici su Twitter

Collegati con noi su LinkedIn

Guarda cosa stiamo facendo su Instagram

Rimani aggiornato iscrivendoti alla nostra newsletter

Sai che ora facciamo anche CORSI? Dai un’occhiata al calendario.


Introduzione

La sfida tra approcci diversi alla missione ultima di realizzare


un’app mobile rimane qualcosa di estremamente complesso da
analizzare. Gli approcci sono sempre stati caratterizzati da due
tipi differenti: ibrido e nativo. Almeno negli ultimi 10 anni, e
ancora oggi, il mercato, le aziende, gli sviluppatori e tutte le parti
in causa che ruotano attorno allo sviluppo mobile valutano quale
strada sia meglio scegliere, e questa decisione è sempre dipesa
da tanti fattori; tra questi, il principale rimane, come
preponderante, quello economico.
Un approccio nativo, almeno in termini aziendali, prevede due
filoni di sviluppo differenti. Sviluppare un’app mobile
nativamente significa sviluppare un’applicazione in Objective-C
o Swift per iOS e Java o Kotlin per Android.
Per quanto sia possibile trovare sviluppatori che governano
entrambe le skill necessarie allo sviluppo per i due principali
sistemi operativi mobili, rimane certamente una sfida piuttosto
ardua che non tutte le aziende scelgono di intraprendere. È
indubbio che le applicazioni native siano più efficienti, in primis
con performance superiori, di quelle ibride; ma, al contempo, è
altrettanto incontestabile che quelle ibride risultino più
sostenibili, sia in fase di sviluppo che di manutenzione.
Queste considerazioni sono, con tutta probabilità, avvalorate
da tutte le alternative nate nell’ultimo decennio; e tra tutte siamo
qua a parlare di quella che a oggi è tra le più promettenti:
Flutter.

Figura I.1 Logo Flutter.

NOTA
Analizzare e confrontare le performance tra nativo e ibrido significa
analizzare anche le performance native divise per linguaggi differenti.
Uso della CPU, GPU, Memory, FPS e consumo della batteria sono alla
base di un benchmark completo. Alcuni benchmark vedono Flutter più
performante di altre alternative ibride ed estremamente competitivo
con applicazioni native.

Nativo e ibrido
Nel caso in cui alcuni di noi si stiano chiedendo quale
effettivamente sia la differenza tra app native e app ibride,
apriamo una brevissima parentesi, in attesa di un
approfondimento nei capitoli successivi. Le app native si
interfacciano direttamente con l’ambiente che le ospita (sistema
operativo) e vengono sviluppate con un software (IDE)
specifico.
Le app ibride, invece, in genere necessitano di uno strato
intermedio, che introduce un calo delle performance e di
conseguenza dei compromessi che introducono problematiche
legate alla user experience e in contesti specifici anche delle
vere e proprie limitazioni.
Possiamo semplicemente immaginare un concetto molto
banale. L’app nativa consente allo sviluppatore di “dialogare”
con il sistema operativo mobile attraverso un sdk, che consente
in tutto e per tutto la gestione del dispositivo. L’approccio ibrido
impone l’utilizzo di un framework di terze parti, che consentirà di
intermediare le funzionalità offerte dal sistema operativo, o
meglio quelle che il framework implementa.
Negli anni ogni framework nato per lo sviluppo di applicazioni
mobili ibride si è prefissato lo scopo di sviluppare un software
che consentisse, con un unico ambiente di sviluppo, di creare
un’applicazione mobile per più sistemi operativi. La sfida si è
sempre caratterizzata sull’implementazione, che risulta essere
una vera e propria firma del framework.
Figura I.2 Differenza sommaria tra app native ed ibride.

I framework ibridi sostanzialmente si sono divisi negli anni tra


approcci differenti. Per esempio, uno dei primi framework ibridi,
chiamato Cordova, utilizzava un layer intermedio per
renderizzare un applicativo web all’interno di una webview e
utilizzava Cordova Plugins per dialogare con il sistema
operativo.
React Native, vera alternativa a Flutter, utilizza un layer
intermedio (Fabric e TurboModules nella nuova architettura) per
renderizzare l’app e dialogare con il sistema operativo.
NOTA
Per webview intendiamo un componente nativo che consente di
eseguire un browser in-app. Di fatto, è possibile visualizzare applicativi
web all’interno della nostra app mobile.

L’architettura di Flutter, a differenza delle altre alternative


ibride citate, non implementa nessun layer intermedio; ciò
consente un incremento delle prestazioni. Il framework Flutter
implementa direttamente le interfacce di UI, chiamate widget e,
oltre a questo, il motore di rendering utilizzato da Flutter disegna
pixel per pixel la user interface sullo schermo del device. React
Native deve interfacciarsi con i componenti nativi per disegnare
la UI; altre alternative ancora utilizzano il browser. Insomma,
questo step ulteriore è in buona parte una caratteristica
distintiva tra Flutter e le alternative.
In Figura I.2 abbiamo riassunto con Platform il layer che
identifica il sistema operativo. La differenza tra le architetture,
rimanendo ad altissimo livello, è che Platform può essere
immaginato come composto a sua volta da due differenti
blocchi: la parte relativa al rendering e quello che offre
l’integrazione con i servizi e corrispettivo hardware. Mentre
React Native, Xamarin o NativeScript dialogano con il sistema
operativo per il rendering dei componenti nativi, Flutter salta a
piè pari questo step in quanto utilizza i propri widget e si
preoccupa lui stesso della fase di rendering.
NOTA
React Native, Xamarin, NativeScript, Ionic sono a oggi i più comuni
framework per applicazioni mobili con architetture differenti e differenti
logiche.

Basti pensare che già l’utilizzo di una UI nativa, come offerto


da alcuni competitor, è stato un grande passo avanti rispetto agli
esordi delle app ibride dove la UI era rappresentata all’interno di
un browser. Credo che sarà il tempo a decidere se questa scelta
distintiva presa dal team di Flutter sarà stata vincente; io credo
di sì e a oggi i numeri legati all’adozione del framework lo
confermano.
Le diverse tecnologie obbligano lo sviluppatore a doversi
adattare ai differenti framework. Lo sviluppatore segue
rigidamente l’ambiente di sviluppo su cui si basa il framework,
utilizzando tutti gli strumenti offerti, e tecnologie tanto differenti
richiedono conoscenze specifiche che difficilmente porteranno
lo sviluppatore a padroneggiarle tutte.
Oltre all’uso del framework in sé, Flutter pone lo sviluppatore
dinanzi alla sfida di un nuovo linguaggio di sviluppo. Molti
framework negli anni si sono basati sul linguaggio “web”:
JavaScript, con la possibilità di utilizzare il superset TypeScript.
Flutter, invece, si basa su un linguaggio sviluppato di recente:
Dart.
NOTA
Conosciuto inizialmente con il nome di Dash, Dart è un linguaggio di
programmazione sviluppato da Google nel 2011 con l’obiettivo di
sostituire JavaScript.

L’applicativo ibrido, qualunque esso sia, se sviluppato con


destrezza potrebbe mascherare all’utente finale che l’applicativo
in uso non è nativo. Per quanto la user experience possa
avvicinarci il più possibile a quella nativa, il problema principale
si annida nelle performance. La nascita di framework per
applicazioni mobili ibridi si è sempre susseguito cercando di
limare le problematiche legate principalmente alle performance.
La tecnologia che è stata leader del settore per anni, Apache
Cordova, sta venendo sempre di più soppiantata dalle “nuove
alternative” proprio perché l’utilizzo di una webview con un
rendering basato su HTML/CSS non è una soluzione che, con la
nascita di applicazioni sempre più complesse, riesce a
soddisfare la user experience che oggi richiede l’utente finale.
NOTA
Apache Cordova è un framework rilasciato open source da Adobe che
a sua volta si basava su PhoneGap creato nel 2009.

Anche spostandosi su soluzioni che non si basano su un


rendering “web”, un layer intermedio che si preoccupa di
dialogare con un componente nativo, indipendentemente dal
framework utilizzato, potrebbe comunque rendere l’app
macchinosa, oltre che lenta. Immaginiamo app complesse:
quanto dispendio di memoria è richiesto per poter renderizzare
tutti gli elementi grafici che compongono il layout? Flutter si
pone esattamente in questo segmento di mercato, offrendo una
soluzione che garantisce performance elevate e una curva di
apprendimento non troppo ampia.
Per quanto l’uso di Flutter possa essere comparato con le sue
alternative, come già anticipato, rispetto all’utilizzo del nativo,
chiaramente l’aspetto vincente si basa sul Time To Market, che
altro non è che il tempo impiegato dall’inizio dello sviluppo di un
prodotto alla sua “commercializzazione”. Statistiche di cui non
sono autore, ma che reputo verosimili, parlano di tempi di
sviluppo dimezzati del 40/50% rispetto allo sviluppo nativo sulle
due piattaforme. Inoltre, oltre ai tempi di sviluppo, si aggiungono
quelli di maintenance e testing che a loro volta vengono
dimezzati. Insomma, per essere schietto e sincero, è più difficile
trovare dei motivi realmente validi per approcciare il nativo, che
viceversa; e sono fermamente convinto che Flutter potrebbe, nel
futuro, minare realmente quello che a oggi rimane territorio
prettamente nativo: games, big data, UI altamente
customizzate, alte prestazioni o app che necessitano di aspetti
specifici legati al sistema operativo.
La sfida resta aperta e c’è chi ha già scommesso che in futuro
Flutter soppianterà di fatto lo sviluppo nativo, ma non ditelo ad
Apple.

Punti di forza
Flutter, rispetto a tutte le soluzioni oggi presenti nel mercato,
riesce a essere particolarmente performante. Rispetto a
competitor che utilizzano uno strato intermedio di
comunicazione con il sistema operativo, Flutter compila
direttamente l’applicativo in linguaggio macchina e questo
dialoga direttamente con il sistema operativo e i vari
componenti. Un approccio di questo tipo rende efficiente
l’applicativo e le sue performance sono molto vicine a quelle che
avremmo con uno sviluppo nativo. Flutter, a oggi, è sicuramente
l’alternativa più performante.

Benchmark
Riporto i dati di un articolo molto ben fatto di una società
ucraina (inVeritaSoft), che ha realizzato diversi benchmark per
sistema operativo, linguaggio e casi d’uso differenti.
NOTA
Di seguito, gli URL dei benchmark: https://inveritasoft.com/blog/flutter-
vs-react-native-vs-native-deep-performance-comparison,
https://inveritasoft.com/blog/flutter-vs-native-vs-react-native-examining-
performance

Dalla comparazione si evince sostanzialmente quello che ci si


aspettava. La scelta nativa rimane la migliore in termini di
prestazioni in caso di animazioni pesanti, ma Flutter dimostra di
essere molto più performante delle alternative, proprio per
quello che ci siamo detti appena sopra.
Consultando altri benchmark, per esempio
https://blogs.perficient.com/2020/11/02/android-app-native-vs-flutter-

vs-react-native/ o https://www.orientsoftware.com/blog/flutter-vs-
react-native-performance/ si raggiunge ugualemente la stessa
conclusione.

Look and feel


Un altro aspetto che si differenzia dalle alternative è la
gestione dei widget. In Flutter,ogni cosa è un widget. Ogni parte,
ogni comportamento, che compone il layout è un widget. Tutto il
codice che scriveremo vivrà all’interno di un widget. I widget,
nonostante possano essere personalizzati, sono di proprietà di
Flutter e questo, come scopriremo, è un bene.
NOTA
Per chi storce il naso poiché preferisce l’approccio self made: sì, è
possibile creare i propri widget. L’uso dei widget creati dalla community
è anch’essa una pratica comune, ma, a differenza di altri package
manager, l’ambiente usato da Flutter ha creato dei criteri valutazione
dei package che attenzionano e aiutano lo sviluppatore nell’adozione
di quest’ultimi.

In genere, non dovremo preoccuparci di come questi


appaiono e se funzionano correttamente su sistemi operativi
mobili differenti, e vi garantisco che non è poca cosa.
Il cosiddetto look and feel è garantito da Google stessa.
Flutter si occuperà di gestire alcuni comportamenti che sono
specifici per sistema operativo, come la navigazione, in cui
troviamo per esempio una differenza per l’animazione della
transizione tra una view e l’altra. L’animazione in questione è
effettivamente diversa tra Android e iOS, ma è solo un esempio
tra i tanti. Oltre a questi comportamenti, specifici per OS, l’uso
dei widget portano con sé un design predefinito; i nostri esempi
si baseranno sul Material design, sia per iOS che per Android.
Flutter offre comunque la possibilità di applicare stili differenti
per sistema operativo; per esempio, offre una serie di widget
con design Cupertino (iOS-style), che potranno essere utilizzati
in ambiente Apple.
Non dover spendere una marea di energie sul look and feel
tra i vari platform è davvero tanta roba. Per non parlare dell’hot
reload, una feature fondamentale durante la fase di sviluppo,
che consente di ricompilare immediatamente il codice
modificato rendendo fruibile la modifica all’interno dell’app.
Un altro aspetto che caratterizza Flutter è l’utilizzo di Skia.
Questo motore grafico consente di renderizzare, molto più
velocemente rispetto ad altre alternative, il layout. L’esperienza
di rendering, oltre a non doversi premurare di come questa
avvenga sulle diverse piattaforme, renderà l’applicazione sarà
incredibilmente fluida e conforme.
NOTA
Skia è una libreria grafica 2D open source. Acquisito da Google nel
2005, questo progetto è diventato il motore grafico 2D di importanti
tools, come Google Chrome e Firefox.

Infine, Flutter non è solo relegato al mondo mobile, ma può


essere utilizzato anche per applicativi web e desktop. Questo
aspetto è una feature molto potente e non del tutto comune,
quanto meno con la facilità con cui è possibile farlo in Flutter. Il
framework consente con estrema facilità di compilare ed
eseguire un’applicazione su platform come Windows, macOS,
Linux, piuttosto che un browser, senza che ci sia la necessità di
modificare il codice sorgente.
Questi aspetti peculiari, che verranno approfonditi nel
dettaglio nei capitoli successivi, hanno reso Flutter un reale
competitor di React Native che, fino a poco tempo fa, dominava
incontrastato il mercato.

Adoption
Flutter è nato a metà del 2017 e, nonostante si basi su un
linguaggio non largamente diffuso come Dart, ha fatto molti
passi avanti in termini di adozione, soprattutto negli ultimi
due/tre anni. Nel survey di stackoverflow del 2022 si può
osservare quanto sia diffuso. Rispetto ai due anni precedenti, la
curva di adozione è notevole: un aumento del 5% di progetti che
usano Flutter (Figura I.3 e Figura I.4).
Google Trends può venirci in aiuto nel capire quanto
effettivamente la curva di interesse su Flutter, dagli addetti ai
lavori e non solo, sia aumentata nel tempo, tanto da dividersi il
mercato con quella che a oggi era la reale alternativa allo
sviluppo di applicazioni ibride.

Figura I.3 Stack Overflow Developer Survey 2022.


Figura I.4 Stack Overflow Developer Survey 2020.

Figura I.5 Google Trends mostra quanto Flutter (blu) partito da 0 nel 2007 nel
giro di pochi anni abbia invertito il trend rispetto a React Native (rosso).

A chi è rivolto Flutter


L’attuale panorama legato al mondo delle app ibride è
piuttosto vasto, ma a farla da padrone è senz’altro React Native,
con Ionic a seguire (anche se molto dietro). Ionic è un altro
framework per app ibride che si basa su Cordova.
React Native è il framework che offre prestazioni più elevate
rispetto ai suoi competitor e ha sostituito Ionic come framework
di riferimento per questa categoria di progetti. Gli sviluppatori
coinvolti su progetti cross-platform hanno una naturale
confidenza con React Native in quanto il linguaggio di
programmazione di riferimento è JavaScript e la familiarità è
fondamentale soprattutto nello start di un nuovo progetto. In
aggiunta, React Native si basa su React che, come sappiamo, è
un noto framework per applicazioni web. Detto ciò, chi ha già
familiarità con React non può che saltare a bordo di React
Native per lo sviluppo di un progetto mobile.
NOTA
React Native è un framework Open Source sviluppato da Meta (ex
Facebook) nel 2015.

React Native si basa su una logica in cui tra il sistema


operativo e il framework c’è un layer intermedio (come in Figura
I.1). Lo strato nativo dialoga con quello javascript tramite questo
layer, che di fatto è il cuore della tecnologia che sta alla base di
React Native. Come abbiamo già anticipato sommariamente,
uno dei punti di forza di Flutter è la compilazione, che rende
totalmente inutile l’utilizzo di strati intermedi di comunicazione.
Google, con Flutter, si è posta nel bel mezzo di questa fetta di
mercato creando una nuova tecnologia, sfruttando così a pieno
la possibilità di realizzare app cross-platform con un paradigma
diverso rispetto a quanto già offerto.
L’obiettivo primario di questa opera è senz’altro quello di
rivolgersi alla più ampia platea possibile. Per quanto molti di noi
rimangono degli intramontabili utilizzatori del desktop, è
insindacabile che un applicativo mobile è più importante di
qualsiasi altra cosa. D’altronde, l’approccio Mobile first è quello
che ha caratterizzato gli ultimi anni. Prima il device mobile nella
progettazione di un applicativo web o mobile app, poi il desktop.
L’approccio Mobile first non ha dettato nessuna regola, ha
semplicemente seguito il suggerimento del mercato. Oggi, gli
utenti che navigano dal dispositivo mobile sono il 20% in più di
quelli che usano il desktop; questo è quanto (Figura I.6).

Figura I.6 Statiche uso dispositivi mobili by Statcounter.com.

Un’amara conclusione, dettata semplicemente dall’anzianità


di chi scrive, è che sono più le applicazioni mobile che non
hanno un corrispettivo web che viceversa. Flutter, tutto
sommato, viene in aiuto con la sua feature di compilazione
cross-platform anche per i futuri nostalgici o per chi ha necessità
legate esplicitamente alla navigazione di un sito internet.
NOTA
Nel caso dello sviluppo di un sito web piuttosto complesso non è detto
che la tecnologia ibrida rimanga comunque valida. Il framework
Angular, a mio parere, rimane la scelta migliore per applicativi web
complessi che trovano limitativo l’uso esclusivo del mobile.
Le esigenze di business hanno tracciato la direzione, ma già
da parecchi anni si predilige un’attenzione maggiore al supporto
mobile. Le aziende e gli sviluppatori mantengono sempre alta la
soglia di interesse per tutte le innovazioni tecnologiche che
portano valore aggiunto. In questi anni, però, molte aziende
hanno fatto a proprio spese l’esperienza di aver scelto la strada
dell’approccio ibrido senza analizzare nel dettaglio quello che la
tecnologia offriva. Applicazioni complesse e ibrido è un
connubio che può rivelarsi pericoloso. Aziende importanti come,
per esempio, AirBnb, hanno fatto la scelta di tornare al nativo
dopo una breve e poco redditizia esperienza con React Native.
Altre aziende, invece, mantenendo uno standard di sviluppo
qualitativamente elevato, sono riuscite a implementare layout,
logiche di business e user experience molto complesse in React
Native con grande successo; un esempio è Uber eats.
Dal mio punto di vista, JavaScript e il compromesso
nell’utilizzo di un layer intermedio limitano di molto quello che
Google con Flutter ha provato ad arginare: nuovo linguaggio e
nessun layer intermedio.
Performance, Code Development Time e time to market
speed sono argomenti che mettono d’accordo sviluppatori e
aziende. Una curva di apprendimento bassa garantisce un
approccio veloce; soprattutto, un framework come Flutter, con le
sue features legate all’utilizzo dei widget, non necessita di skill
elevate e profondamente mature: mi riferisco all’uso di
JavaScript in progetti di grosse dimensioni. Quando si parla di
progetti importanti, in cui gli sviluppatori riescono a portare a
casa un buon risultato senza aver perso anni di vita lungo il
percorso, significa che abbiamo trovato la giusta confidenza con
la tecnologia.
La sfida che mi pongo con questo volume è proprio questa.
Neofita, sviluppatore nativo, sviluppatore ibrido, scopriamo
insieme il valore aggiunto di Flutter e comprendiamo insieme i
suoi punti di forza e le sue debolezze, fino a capire perché
Google si sia mossa in questa direzione.
Il livello di dettaglio di questo volume sarà piuttosto alto
cercando di non dare nulla per scontato, provando a garantire
una facile lettura e comprensione, anche per i non avvezzi allo
sviluppo. In poche parole sarà una sfida piuttosto ardua.
La tecnologia in questione è recente, quindi sarà qualcosa di
nuovo sia per uno sviluppatore che per un novellino.
Muoveremo i primi passi verso Flutter, approfondendo i lati più
tecnici e interni alla tecnologia, e studieremo il linguaggio di
sviluppo Dart. Durante il percorso, insieme, svilupperemo la
nostra prima vera app.

I revisori
Ralf Mureddu è Software Engineer specializzato in sicurezza
applicativa con esperienza più che decennale in ambito
finanziario.
Carlo Zappatini è Software Engineer specializzato in
applicazioni mobile e front-end con un passato da
programmatore embedded in ambito automotive.

Il codice
Tutto il codice sorgente presente in questo libro è disponibile
all’indirizzo https://bit.ly/apo-saf e su GitHub all’indirizzo
https://github.com/vgflutter/flutter_apogeo.
Ringraziamenti
Il ringraziamento principale va ai lettori, che spero con tutto il
cuore siano soddisfatti. Non è la mia prima esperienza in questo
campo, tutt’altro, e ogni lavoro rende il prossimo sempre più
sfidante. Alzare l’asticella nella speranza di far sempre meglio è
l’obiettivo che mi sono prefissato. Nonostante l’ultimo anno non
sia stato particolarmente felice, ho portato avanti il lavoro con il
massimo impegno e non ho rimpianti: ho fatto del mio meglio.
Quindi, grazie a voi per aver dedicato tempo e attenzione al mio
lavoro.
Un altro grazie ai miei fedelissimi: a mia moglie per la
pazienza e alla coppia Ralf Mureddu e Carlo Zappatini per il
lavoro di revisione che hanno condotto cercando di complicarmi
la vita il più possibile. Grazie davvero.
Infine, un saluto a tutti i colleghi di Cornèr Banca SA.
Nonostante la quotidianità lavorativa sia in antitesi con il lavoro
da autore, un ambiente di lavoro stimolante come quello che mi
trovo a vivere giornalmente aiuta non poco a mantenere alto il
morale e la voglia di fare.
Detto ciò, credo fermamente che Flutter diventerà il
riferimento principale nel mondo del cross-platform
development, e mi auguro che riguardo il tema Flutter questo
sia solo un arrivederci.
Contattatemi pure via Linkedin
https://www.linkedin.com/in/vincenzo-giacchina-1550538b o via mail

(giacchina.vincenzo@gmail.com): sarò felice di ricevere feedback,


domande e, nel mio piccolo, proverò a darvi supporto.
Grazie ancora e a presto.
Capitolo 1

Installazione

Mettere in piedi un ambiente di sviluppo che consenta di


utilizzare Flutter non è poi così difficile. La complessità di
un’attività di questo tipo è strettamente collegata a diversi fattori:
il tipo di applicativo che vogliamo realizzare, il sistema operativo
che ci ospita, il sistema operativo target, tutto quello che ruota
intorno al framework che stiamo utilizzando e tutte le
dipendenze richieste. Un applicativo mobile, cross platform, non
è di certo paragonabile a una landing page, ma rispetto ad altre
soluzioni Flutter ci darà una mano anche in questo.
L’obiettivo di questo capitolo è spiegare piuttosto velocemente
in che cosa consiste l’installazione dell’ambiente. Eviteremo di
andare troppo nel dettaglio: per quello affidatevi, di certo senza
sbagliare, al sito ufficiale, https://flutter.dev/docs/get-
started/install.

Detto questo, iniziamo a sporcarci le mani.


Il primo passo da fare è procedere al download di flutter SDK.
NOTA
Software Development Kit (SDK) è una suite di tools realizzati per lo
sviluppo di software.

NOTA
I possessori macOS con processori Apple Silicon, per esempio M1,
necessitano di una dipendenza aggiuntiva (/usr/sbin/softwareupdate --
install-rosetta --agree-to-license), oltre a una versione di SDK Flutter
dedicata.

Terminato il download, procediamo a scompattare l’archivio e


a registrare il path di Flutter affinché possiamo richiamarlo in
semplicità da qualsiasi punto del filesystem.
Fate attenzione a eseguire il comando ponendo un livello
sotto la directory flutter appena scompattata.
Per Mac e Linux:
# export PATH="$PATH:`pwd`/flutter/bin"

NOTA
Con il comando export impostiamo il path solo all’interno dell’istanza
della console. Per rendere permanente il path, possiamo inserire il
path assoluto nel file /etc/paths.

NOTA
Per Linux, nel caso in cui l’utente utilizzi bash, saprà che è sufficiente
aggiungere il path di Flutter nel file: $HOME/.bashrc.

Per Windows, dopo aver scompattato il file archivio di Flutter,


sarà necessario registrare il suo path tramite il comando env
che può essere eseguito dal menu Start. È possibile perseguire
l’aggiunta del path di Flutter tramite l’uso del tool: Edit
environment variables for your account.

Flutter doctor
Terminata l’installazione, proseguiamo con la configurazione
dell’ambiente di sviluppo. Il passo successivo si basa sull’uso
del tool doctor che consente di verificare a che punto si trova la
nostra installazione.
# flutter doctor
Doctor summary (to see all details, run flutter doctor -v):
[✓] Flutter (Channel stable, 3.7.1, on macOS 12.3.1 21E258 darwin-x64,
locale it-CH)
[X] Android toolchain - develop for Android devices
X Unable to locate Android SDK.
Install Android Studio from:
https://developer.android.com/studio/index.html
On first launch it will assist you in installing the Android SDK
components.
(or visit https://flutter.dev/docs/get-
started/install/macos#android-setup
for detailed instructions).
If the Android SDK has been installed to a custom location, set
ANDROID_SDK_ROOT to that location.
You may also want to add it to your PATH environment variable.

[X] Xcode - develop for iOS and macOS


X Xcode installation is incomplete; a full installation is necessary
for iOS
development.
Download at: https://developer.apple.com/xcode/download/
Or install Xcode via the App Store.
Once installed, run:
sudo xcode-select --switch
/Applications/Xcode.app/Contents/Developer
sudo xcodebuild -runFirstLaunch
X CocoaPods not installed.
CocoaPods is used to retrieve the iOS and macOS platform side's
plugin
code that responds to your plugin usage on the Dart side.
Without CocoaPods, plugins will not work on iOS or macOS.
For more info, see https://flutter.dev/platform-plugins
To install:
sudo gem install cocoapods
[!] Android Studio (not installed)
[!] VS Code (version 1.50.1)
X Flutter extension not installed; install from
https://marketplace.visualstudio.com/items?itemName=Dart-
Code.flutter
[!] Connected device
! No devices available

! Doctor found issues in 5 categories.


In questo caso, Flutter ci notifica che dovremmo procedere a
un aggiornamento per via di una release più recente.
Procediamo pertanto all’aggiornamento tramite il comando:
# flutter upgrade

Terminato questo step, è necessario preoccuparsi delle


dipendenze mancanti. In questa specifica installazione ci viene
notificato che mancano Android SDK e Xcode, entrambi
necessari per sviluppare app per Android e iOS. Inoltre,
dovremo anche gestire l’IDE che useremo, ma affronteremo il
tema tra un attimo.
NOTA
Il comando channel consente di scegliere tra i canali di release: stable,
beta e master. Il consiglio è verificare ed eventualmente scegliere il
channel stable, che rappresenta la linea di sviluppo da usare per la
produzione, in quanto è la più stabile.

Android
Come anticipato, Flutter ci notifica la mancanza della
toolchain Android.
NOTA
Toolchain è un nsieme di software che lavorano in sinergia per lo
sviluppo del software.

Il primo passo da fare è procedere al download di Android


Studio (https://developer.android.com/studio). Android Studio è un
IDE sviluppato da Google necessario allo sviluppo Android.
NOTA
IDE è un software che fornisce un ambiente di sviluppo per uno o più
linguaggi di programmazione.
Come mostrato in Figura 1.1, il wizard di installazione
procede per conto nostro, installando le dipendenze necessarie
per il corretto uso dell’IDE Android Studio e di conseguenza per
Flutter.
Dopo l’installazione di Android studio, proseguiamo
nell’accettare la licenza legata ad Android tramite il comando
flutter:
# flutter doctor --android-licenses

Figura 1.1 Android Studio Setup Wizard.

Nel caso in cui il comando dia un errore relativo a un tool


mancante: sdkmanager, procediamo facendo attenzione che la
command line tools dell’sdk sia installata. È possibile verificare
questa dipendenza in Android Studio, nel tab SDK Tools
nell’SDK Manager.
Ora, se rilanceremo il comando flutter doctor, potremo
leggere che siamo pronti per lo sviluppo in Android, avendo
installato la toolchain.
[✓] Android toolchain - develop for Android devices (Android SDK
version 30.0.2)

Procediamo integrando Flutter, tramite plugin, con Android


Studio per soddisfare i seguenti requisiti:
[!] Android Studio (version 4.1)
X Flutter plugin not installed; this adds Flutter specific
functionality.
X Dart plugin not installed; this adds Dart specific functionality.
[!] VS Code (version 1.50.1)
X Flutter extension not installed; install from
https://marketplace.visualstudio.com/items?itemName=Dart-
Code.flutter

Per risolvere anche questa dipendenza, sarà necessario


avviare Android Studio e procedere all’installazione del plugin
Flutter e Dart.
Figura 1.2 Plugin Flutter per Android Studio.

Se volessimo utilizzare un IDE differente, Flutter supporta


anche VS Code (Visual Studio Code), che è quello che
attualmente uso con soddisfazione e vi consiglio caldamente.
Nel mio caso, Flutter ha rilevato l’installazione di Visual Studio,
ma attualmente non supporta Flutter.
Avviamo VS Code, oppure installiamolo se non è già presente
nel nostro ambiente, (è disponibile all’indirizzo
https://code.visualstudio.com/) e tramite le preference procediamo

a installare le estensioni Flutter e Dart.


Adesso la procedura di installazione per Android risulta
essere terminata e flutter doctor lo confermerà.
[✓] Flutter (Channel stable, 1.22.3, on Mac OS X 10.15.5 19F101, locale
it-CH)
[✓] Android toolchain - develop for Android devices (Android SDK
version 30.0.2)
[✓] Android Studio (version 4.1)
[✓] VS Code (version 1.50.1)

Figura 1.3 Plugin Flutter per VS Code.

NOTA
Per utilizzare il device fisico Android durante il processo di sviluppo è
necessario procedere all’installazione del driver Google USB,
reperibile a questo indirizzo:
https://developer.android.com/studio/run/win-usb.

iOS
Come ben sappiamo, e se così non fosse potreste
tranquillamente immaginarlo dalle politiche Apple, per
sviluppare app per iOS è necessario avere un Mac, proprio
perchè il corrispettivo di Android Studio per iOS è Xcode, e
quest’ultimo è disponibile solo per Apple.
Procediamo all’installazione di Xcode tramite Apple Store
(https://developer.apple.com/xcode/).
Una volta ottenuto l’IDE, procedere avviandolo per accettare
la licenza e l’installazione delle dipendenze, che verrà fatta dal
software automaticamente. Insieme a Xcode sarà necessario
installare anche CocoaPods.
CocoaPods (https://cocoapods.org/) consente di gestire le
dipendenze; nel caso di Flutter potrebbe tornare utile nel caso si
usino plugin che necessitano di dipendenze.
# sudo gem install cocoapods

Una volta terminata l’installazione, eseguiamo Xcode.app


(può essere eseguito nelle nostre applicazioni). Se prima
dell’avvio ci venisse chiesto di aggiornarlo o scaricare delle
dipendenze, proseguiamo pure dando il nostro consenso. Una
volta avviato, ci apparirà una finestra come quella in Figura 1.4.
Figura 1.4 Xcode.

Emulazione
La connessione di un device mobile, o la sua emulazione, è
parte fondamentale nella preparazione dell’ambiente di
sviluppo; infatti, doctor lo segnala inequivocabilmente:
[!] Connected device
! No devices available

L’approccio alla creazione di un device virtuale, che altro non


è che un simulatore, sarà differente per OS. Nel caso di
Android, per semplicità procediamo alla configurazione tramite
Android Studio. È possibile procedere anche tramite il tool
offerto dall’SDK via command line.

ADV
È comunque possibile creare e gestire i device virtuali tramite
il tool emulator offerto da Android SDK, installato
precedentemente. Se il path dell’SDK Android vi fosse sfuggito
durante il wizard di installazione di Android Studio, è comunque
consultabile eseguendo Android Studio e selezionando l’SDK
Manager nella lista delle Preferences (Figura 1.5).
Tramite la GUI di Android Studio è possibile lanciare
l’interfaccia Android Virtual Device Manager. Di default
dovremmo già trovare un device; infatti, avviandolo tramite
l’icona di start e procedendo a lanciare flutter doctor, noteremo
che adesso il device viene rilevato con successo.
[✓] Connected device (1 available)

Nel caso incontraste problemi nell’avvio del device vi consiglio


di controllare lo spazio sufficiente su disco, in quanto il software
da installare non è poco.

Figura 1.5 SDK Manager di Android Studio.

In questa istanza di installazione, procediamo pure al


download e di conseguenza alla configurazione del device
virtuale di Android API 33, l’ultima versione Android attuale.
Le API Level di Android, come è possibile intuire dalla Figura
1.6, sono specifiche per ogni versione del sistema operativo
Android. Se utilizzassimo le API 33, faremmo riferimento alle
API offerte dal sistema operativo Android 13, conosciuto come
Tiramisù. Le API dell’SDK Android offrono un innumerevole set
di interfacce per interagire con il sistema operativo Android e
garantiscono la retrocompatibilità, per questa ragione iniziare a
sviluppare con l’ultima versione stabile delle API Level di
Android è cosa giusta e consigliata.
Come mostrato in Figura 1.6, dall’interfaccia di AVD potete
sbizzarrirvi a creare i device virtuali che preferite; faremo
qualche altro esempio lungo il percorso.
Nel caso in cui si volesse approcciare la gestione dei device
Android da command line, questo sarebbe possibile tramite il
tool: avdmanager.
Figura 1.6 Device virtuale in esecuzione.

È importante, nel caso in cui invece si volesse utilizzare un


device Android fisico, abilitare la modalità debug, affinché il
device venga rilevato; ciò consente di eseguire l’applicazione
Android sul vostro device. Potete visionare la documentazione
ufficiale per abilitare questo supporto all’indirizzo
https://developer.android.com/studio/debug/dev-options.

Brevemente, in genere, si procede ad abilitare la modalità


debug tramite il menu Impostazioni, su Info telefono e poi
facendo clic ripetutamente sul numero di build di Android, fino a
ottenere l’attivazione del menu Opzioni sviluppatore, dove sarà
possibile attivare il debug USB. Fatto ciò, il vostro IDE sarà in
grado di eseguire l’applicazione sul device fisico Android.

Simulator
L’installazione di Xcode si porta con sé il tool simulator.
L’emulatore di Xcode è facilmente eseguibile una volta creato il
nostro progetto all’interno di Xcode. In questo caso, opteremo
per eseguire direttamente il simulatore, fuori lo scope di Xcode,
proprio per verificare che tutto l’ambiente è disponibile.
NOTA
La differenza tra emulatore e simulatore è che il primo si prefigge lo
scopo di emulare anche le risorse hardware, a differenza dei simulatori
che simulano esclusivamente il sistema operativo. Pertanto risulta
sempre fondamentale, anche in fase di sviluppo, il supporto del device
fisico per effettuare i nostri test.

Cerchiamo nel nostro finder il tool Simulator.app ed


eseguiamolo. Verrà immediatamente simulato un iPhone (vedi
Figura 1.7). In ogni caso è possibile, molto semplicemente,
scegliere quale device virtuale avviare.
Flutter, tramite doctor, potrà confermarci la connessione al
device virtuale.
# flutter doctor -v

[✓] Connected device (3 available)
• iPhone 13 mini (mobile) • 4C25CEB5-6DA1-4D4D-9038-D0AFD725DA41 •
ios • com.apple.CoreSimulator.SimRuntime.iOS-15-0 (simulator)

Rispetto a quanto visto per Android, il deploy sul device fisico


iOS segue una logica differente con passaggi differenti.
Nonostante questo, è necessario, anche sul device Apple,
attivare la modalità developer e fidarsi del certificato del Mac a
cui stiamo collegando il nostro device. Aimè, è necessario
procedere all’acquisto di un account sviluppatore Apple ed è
anche necessario configurare i certificati di sviluppo e
distribuzione associati al vostro account sviluppatore Apple.
NOTA
In fase di sviluppo, non è necessario acquistare un account da
sviluppatore per lanciare l’app sul device fisico. Possiamo utilizzare
semplicemente l’Apple ID creando con esso un account in Xcode,
sezione Account in Preferenze. Fatto ciò, utilizzeremo questo account
come Team nella sezione identity di Xcode.

Non tratterò questo tema per mancanza di tempo, e purtroppo


vi anticipo che richiede un pò di impegno, in quanto Apple,
rispetto ad Android, ha implementato una gestione del deploy
per il mondo Apple che non è semplice come la principale
concorrenza. Vi affido a questa guida
https://docs.flutter.dev/deployment/ios e al sito ufficiale per lo

sviluppo Apple: https://developer.apple.com/.


Figura 1.7 IPhone 13 mini virtualizzato.
Capitolo 2

Hello world

Procediamo spediti con quello che nel mondo dello sviluppo è


riconosciuto come il primo obiettivo di uno sviluppatore che sta
approcciando un nuovo linguaggio. Noi non ci esimiamo e
facciamo altrettanto.
$ flutter create hello_world

Creating project hello_world...


Running "flutter pub get" in hello_world... 1'750ms
Wrote 127 files.

All done!
In order to run your application, type:

$ cd hello_world
$ flutter run

Your application code is in hello_world/lib/main.dart.

$ cd hello_world/
$ flutter run
Multiple devices found:
macOS (desktop) • macos • darwin-x64 • macOS 12.3.1 21E258 darwin-
x64
Chrome (web) • chrome • web-javascript • Google Chrome 107.0.5304.110
[1]: macOS (macos)
[2]: Chrome (chrome)
Please choose one (To quit, press "q/Q"): 1
Launching lib/main.dart on macOS in debug mode...
Building macOS application...
Syncing files to device macOS... 418ms

Flutter run key commands.


r Hot reload.
R Hot restart.
h List all available interactive commands.
d Detach (terminate "flutter run" but leave application running).
c Clear the screen
q Quit (terminate the application on the device).

Running with sound null safety

An Observatory debugger and profiler on macOS is available at:


http://127.0.0.1:56693/Ygp8yXVtgW4=/
The Flutter DevTools debugger and profiler on macOS is available at:
http://127.0.0.1:9100?uri=http://127.0.0.1:56693/Ygp8yXVtgW4=/

Desktop
L’output appena prodotto mostra inizialmente la creazione del
progetto tramite il comando create. L’output, in questo caso,
mostra l’esecuzione di un sottocomando flutter pub get, che è il
comando per la gestione (download) delle dipendenze. pub.dev
(https://pub.dev/) è il package manager di Dart, usato in Flutter
per gestire le dipendenze.
Il secondo step è l’esecuzione dell’applicazione hello_world,
un progetto di esempio utile per approcciare la nostra prima
applicazione in Flutter. Il comando run ci chiede se emulare
l’applicativo su un browser o sul desktop, due scelte che di
default non necessitano di nessuna configurazione particolare.
In questo caso, procediamo con la prima (Desktop), come in
Figura 2.1.
Figura 2.1 Flutter in emulazione sul desktop.

È possibile seguire i link http, come mostrato dall’output


(Figura 2.2 e Figura 2.3), per accedere agli strumenti di debug
offerti da Flutter. In ordine, abbiamo Observatory e Devtools. Il
primo è uno strumento offerto dall’SDK Dart per il debug single-
stepping e il profiling. Il secondo è più orientato al debug della
UI. Entrambi sono strumenti potenti che ci consentono di
analizzare in profondità lo stato della nostra applicazione.
Figura 2.2 Observatory debugger.

Figura 2.3 Flutter DevTools debugger.

NOTA
Con il termine single-stepping si intende una funzionalità di debug
dove lo sviluppatore esegue debug del codice linea per linea istruendo
il debugger a fermarsi esattamente nella linea di codice desiderata
tramite un breakpoint.
Browser
Procediamo eseguendo nuovamente il run e avviando
l’esecuzione sul browser.
$ flutter run
Multiple devices found:
macOS (desktop) • macos • darwin-x64 • macOS 12.3.1 21E258 darwin-
x64
Chrome (web) • chrome • web-javascript • Google Chrome 107.0.5304.110
[1]: macOS (macos)
[2]: Chrome (chrome)
Please choose one (To quit, press "q/Q"): 2
Launching lib/main.dart on Chrome in debug mode...
Waiting for connection from debug service on Chrome... 25.1s
This app is linked to the debug service:
ws://127.0.0.1:60458/jXWpO1eDMlk=/ws
Debug service listening on ws://127.0.0.1:60458/jXWpO1eDMlk=/ws

Running with sound null safety

To hot restart changes while running, press "r" or "R".


For a more detailed help message, press "h". To quit, press "q".

An Observatory debugger and profiler on Chrome is available at:


http://127.0.0.1:60458/jXWpO1eDMlk=
Flutter Web Bootstrap: Programmatic
The Flutter DevTools debugger and profiler on Chrome is available at:
http://127.0.0.1:9100?uri=http://127.0.0.1:60458/jXWpO1eDMlk=
Flutter Web Bootstrap: Programmatic

Come è facile intuire, l’avvio dell’app prevede le stesse


logiche che abbiamo visto per il desktop, con gli stessi strumenti
di debug.
Figura 2.4 Chrome.

Device
Proseguiamo con l’esecuzione dell’applicazione,
assicurandoci di avere un emulatore Android attivo, o iOS se
preferite.

Android
Nel mio caso, possiamo usare direttamente da linea di
comando per l’emulatore Android, piuttosto che l’IDE.
Per farlo abbiamo due modi: utilizzare lo strumento emulator
di Android SDK oppure direttamente Flutter.
$ ./Library/Android/sdk/tools/emulator -list-avds
Pixel_API_33
$ ./Library/Android/sdk/tools/emulator -avd Pixel_API_33

o
$ flutter emulators
2 available emulators:

apple_ios_simulator • iOS Simulator • Apple • ios


Pixel_API_33 • Pixel API 33 • Google • android

To run an emulator, run 'flutter emulators --launch <emulator id>'.


To create a new emulator, run 'flutter emulators --create [--name xyz]'.

You can find more information on managing emulators at the links below:
https://developer.android.com/studio/run/managing-avds
https://developer.android.com/studio/command-line/avdmanager
$ flutter emulators --launch Pixel_API_33

Una volta avviato l’emulatore, eseguiamo l’applicazione.


# flutter run

Using hardware rendering with device sdk gphone64 x86 64. If you notice
graphics artifacts, consider enabling software rendering with "--enable-
software-rendering".
Launching lib/main.dart on sdk gphone64 x86 64 in debug mode...
Checking the license for package Android SDK Build-Tools 30.0.3 in
/Users/vgiacchina/Library/Android/sdk/licenses
License for package Android SDK Build-Tools 30.0.3 accepted.
Preparing "Install Android SDK Build-Tools 30.0.3 (revision: 30.0.3)".
"Install Android SDK Build-Tools 30.0.3 (revision: 30.0.3)" ready.
Installing Android SDK Build-Tools 30.0.3 in
/Users/vgiacchina/Library/Android/sdk/build-tools/30.0.3
"Install Android SDK Build-Tools 30.0.3 (revision: 30.0.3)" complete.
"Install Android SDK Build-Tools 30.0.3 (revision: 30.0.3)" finished.
Checking the license for package Android SDK Platform 31 in
/Users/vgiacchina/Library/Android/sdk/licenses
License for package Android SDK Platform 31 accepted.
Preparing "Install Android SDK Platform 31 (revision: 1)".
"Install Android SDK Platform 31 (revision: 1)" ready.
Installing Android SDK Platform 31 in
/Users/vgiacchina/Library/Android/sdk/platforms/android-31
"Install Android SDK Platform 31 (revision: 1)" complete.
"Install Android SDK Platform 31 (revision: 1)" finished.
Running Gradle task 'assembleDebug'... 228.9s
✓ Built build/app/outputs/flutter-apk/app-debug.apk.
Installing build/app/outputs/flutter-apk/app.apk... 9.1s
Syncing files to device sdk gphone64 x86 64... 990ms

Flutter run key commands.


r Hot reload.
R Hot restart.
h List all available interactive commands.
d Detach (terminate "flutter run" but leave application running).
c Clear the screen
q Quit (terminate the application on the device).

Running with sound null safety


An Observatory debugger and profiler on sdk gphone64 x86 64 is available
at: http://127.0.0.1:61221/_AONi7Nc8Xk=/
The Flutter DevTools debugger and profiler on sdk gphone64 x86 64 is
available at: http://127.0.0.1:9100?
uri=http://127.0.0.1:61221/_AONi7Nc8Xk=/

L’applicazione verrà eseguita con estrema semplicità. Diamo


uno sguardo al nostro emulatore per renderci conto di che cosa
sia successo (Figura 2.5).

Figura 2.5 Android.


iOS
Il passo che facciamo adesso, prima di addentrarci in Flutter,
è proseguire, rispetto a quanto visto, con il corrispettivo per iOS.
L’unica esigenza per soddisfare quest’altro passaggio è avere
un secondo simulatore avviato, pertanto procediamo
avviandone uno.
# open -a Simulator

o
$ flutter emulators --launch apple_ios_simulator

A questo punto procediamo con il run di Flutter e scegliamo il


nostro simulatore di riferimento.
# flutter run

Multiple devices found:


sdk gphone x86 arm (mobile) • emulator-5554 •
android-x86 • Android 11 (API 30) (emulator)
iPhone 12 Pro Max (mobile) • 60C4468F-AD72-48D7-9C6C-A321BECBEC2E • ios
• com.apple.CoreSimulator.SimRuntime.iOS-14-1 (simulator)
[0]: sdk gphone x86 arm (emulator-5554)
[1]: iPhone 12 Pro Max (60C4468F-AD72-48D7-9C6C-A321BECBEC2E)
Please choose one (To quit, press "q/Q"): 1
Launching lib/main.dart on iPhone 12 Pro Max in debug mode...

Running Xcode build...


└─Compiling, linking and signing... 76.7s
Xcode build done. 108.7s
Waiting for iPhone 12 Pro Max to report its views... 5ms
Syncing files to device iPhone 12 Pro Max... 416ms

Flutter run key commands.


r Hot reload.
R Hot restart.
h Repeat this help message.
d Detach (terminate "flutter run" but leave application running).
c Clear the screen
q Quit (terminate the application on the device).
An Observatory debugger and profiler on iPhone 12 Pro Max is available
at: http://127.0.0.1:55490/6Th8kkJCwWI=/
Figura 2.6 iPhone.

Hello World
È arrivato il momento di buttare un occhio dentro al progetto
hello_world. Facciamolo direttamente da VS Code.
Il primo passo è quello di modificare il file lib/main.dart,
sostituendo la stringa You have pushed the button this many times:

con Hello World, tanto per concludere il capitolo vantandoci di


aver scritto la prima riga di codice.
Grazie all’installazione del plugin, potremo effettuare il run di
flutter dal Run and Debug di VS Code. Inoltre, nella barra
dedicata a Flutter posizionata nel bottom dell’IDE, è possibile
scegliere quale dispositivo emulare. Per eseguire l’app è
necessario posizionarsi nel file main.dart e lanciare la build
attraverso il menu di sinistra Esegui con debug, o utilizzare
l’icona in alto alla sinistra del tab main.dart.
Una volta avviata l’app dopo la modifica, questa verrà
compilata ed eseguita nuovamente nell’emulatore. Questa
feature è chiamata hot reload ed è estremamente reattiva.

Figura 2.7 VS Code.


Progetto
Il progetto hello_world ha un’alberatura molto semplice.
Analizziamo sommariamente la struttura, ispezionando la
directory hello_world.

README.md: File di testo che introduce e spiega brevemente il


progetto.
android: Directory destinata alla piattaforma Android.

ios: Directory destinata alla piattaforma iOS.


linux: Directory destinata alla piattaforma Linux-app per
Desktop
pubspec.lock: File creato per tracciare tutte le versioni delle
dipendenze del progetto
test: Directory destinata ai test.

windows: Directory destinata alla piattaforma Windows - app


per Desktop.
analysis_options.yaml: File per configurare l’analisi statica del
codice (analyzer).
hello_world.iml: File di modulo generato da Flutter per il

supporto all’ editor IntelliJ IDEA.


lib: Directory con il sorgente del nostro applicativo

macos: Directory destinata alla piattaforma Mac OS X - app


per Desktop
pubspec.yaml: File che contiene i metadata del progetto come

le dipendenze specifiche
web: Directory destinata alla piattaforma web - app per

Browser
.idea: Directory che contiene i metadata del workspace per
l’editor IntelliJ IDEA
.gitignore: File che indica a GIT che file o directory ignorare

.metadata: File usato da Flutter stesso che contiene i


metadata del progetto
.dart_tool: Directory usata dai Dart tools

NOTA
GIT è un software per il controllo di versione distribuito (https://git-
scm.com/).

I punti di attenzione rispetto a quanto brevemente elencato,


che verranno trattati nel dettaglio, sono la gestione delle
dipendenze con pub.dev e certamente la directory lib, in quanto
depositaria del codice sorgente.

Build
Il comando flutter run esegue una build in modalità debug.
Per gli amanti delle definizioni, sperando di non averlo dato per
scontato, con il termine build si intende il processo che
trasforma il codice sorgente in un artefatto che potrà essere
eseguito.

Debug
La modalità debug consente di velocizzare lo sviluppo, grazie
a hot reload, e di conseguenza anche l’esecuzione sul device
fisico o virtuale. Inoltre, vengono forniti in questa modalità tutti
gli strumenti di debug, come devtools.
In questa modalità sarà possibile ispezionare il layout,
esaminare nel dettaglio tutti i componenti che compongono la
UI, analizzare codice, memoria, profilazione e tutti gli strumenti
necessari agli sviluppatori.

Release
La modalità release, flutter run –release, consente di effettuare
una build destinata a una release, ovvero alla distribuzione
finale dell’applicazione. In questa modalità la build sarà
ottimizzata al massimo garantendo anche la dimensione più
piccola possibile dell’eseguibile. Tutti gli strumenti di debug non
saranno forniti.
Inoltre, per le applicazioni destinate al web, l’applicazione
verrà minificato.
NOTA
La minificazione è un processo di compressione del codice sorgente.
Viene usata per il web per ridurre le dimensioni dei sorgenti, in genere
JS e CSS.

Profile
La modalità profile, flutter run –profile, a differenza di quella
di debug, è dedicata alla profilazione, ovvero la possibilità di
misurare le performance dell’applicazione; infatti, sarà possibile
attivarla solo su device fisici, non virtuali.
DevTools saranno comunque presenti in questa modalità e
potranno essere visionati in dettaglio sul tab Performance.
In conclusione, dopo aver parlato di build, diventa essenziale,
prima ancora di procedere con lo studio di Flutter, introdurre
Dart, il linguaggio di sviluppo di Flutter!
Capitolo 3

Dart in teoria

Dart è un linguaggio creato da Google nel 2011. Nei primi


anni non è riuscito a ritagliarsi uno spazio ben definito, ma
probabilmente ancora i tempi non erano maturi. Da qualche
anno, grazie a Flutter, la situazione è notevolmente cambiata.
Dart è nato come alternativa a JavaScript che, con lo sviluppo
del web e il conseguente evolversi di applicativi web sempre più
complessi, a detta di molti, risulta essere poco performante e
particolarmente soggetto a problematiche di sicurezza.
Gli aspetti fondamentali del linguaggio Dart che lo rendono
molto appetibile sono sicuramente le performance, la curva di
apprendimento, la garanzia Google, la lungimiranza legata a
nuovi progetti Google (Flutter e Fuchsia tra tutti) e il suo
dinamismo legato alla compilazione AoT o JIT. Le due
compilazioni supportate da Dart sono Ahead-of -time e Just-in-
time, che corrispondono a due tipi differenti di compilazione. Il
primo prevede la trasformazione da codice sorgente a binario,
utilizzata per esempio per la distribuzione di applicazioni binarie
per iOS e Android. La compilazione JIT, invece, viene eseguita
a runtime, per esempio durante la fase di sviluppo. Il reload
della stringa Hello World, visto precedentemente, si basa
esattamente su questo principio.
Nonostante Dart sia stato partorito più di 10 anni fa, non ha
obiettivamente preso piede tanto quanto probabilmente ci si
aspettava. L’obiettivo di Google era renderlo il nuovo JavaScript,
ma probabilmente, vista la diffusione di quest’ultimo, questa era
ed è, ancora oggi, una missione impossibile. Sebbene in casa
Google il linguaggio sia molto usato, è grazie a Flutter che si è
diffuso esponenzialmente negli ultimi anni.
Dart, esattamente come JavaScript, è un linguaggio che trova
maggiore campo d’azione nella parte client, ma non solo. Grazie
alle potenzialità offerte dal linguaggio stesso, si sta iniziando a
valutare Dart anche come base per applicazioni server side. Ciò
a cui faccio riferimento non è la semplice applicazione
command line, ma piuttosto, per intenderci, una vera alternativa
a Node.js.
NOTA
Node.js è un open Source server side environment basato su
JavaScript.

Aspetti peculiari
Le potenzialità del linguaggio Dart sono state
specificatamente create per sopperire a quello che JavaScript
non può offrire; mi riferisco alle sue caratteristiche intrinseche.
La storia vuole che la prima prova scritta dell’esistenza di
Dart, al tempo solo teorizzato, venga ritrovata in una mail
invitata da Mark S. Miller a una mailing list interna a Google e
poi trapelata. La mail incriminata parlava di quanto emerso
durante un summit aziendale che vedeva tra i vari partecipanti
diversi team di sviluppo, ovvero la necessità di contribuire allo
sviluppo di un nuovo linguaggio (chiamato inizialmente Dash).
L’obiettivo era sostituire JavaScript, garantendo prestazioni
migliori nell’utilizzo in progetti di dimensioni medio-grandi.
La necessità di un nuovo linguaggio partiva dal presupposto
che (riporto testualmente una frase significativa): “Javascript
has fundamental flaws that cannot be fixed merely by evolving
the language”. Da questo presupposto nasce in Google la
scommessa dello sviluppo di questo nuovo linguaggio.
Uno degli aspetti peculiari di Dart è la sua compilazione che
può essere eseguita in modalità differenti. Inizialmente, il
progetto prevedeva lo sviluppo di una Dart VM (Virtual Machine)
e una sua integrazione in Chrome, per intenderci un
corrispettivo di V8 (JavaScript engine). Nel 2015 questo
progetto viene ufficialmente abbandonato e viene data priorità
all’integrazione tramite una feature del compilatore per generare
codice JavaScript affinché si possano far girare, di fatto,
applicazioni scritte in Dart nei browser.
NOTA
La Virtual Machine in ambienti di sviluppo è un software che fornisce
un ambiente di esecuzione in cui eseguire codice ad alto livello.

Dart SDK è una parte fondamentale del linguaggio. L’SDK


fornisce una Virtual Machine che consente di eseguire codice
Dart grazie alla sua Dart VM. Dart SDK è fornito come bundle in
Flutter. Possiamo infatti, esattamente come con il comando
flutter, richiamare anche dart (stesso path environment).

# dart

A command-line utility for Dart development.

Usage: dart <command|dart-file> [arguments]

Global options:
-h, --help Print this usage information.
-v, --verbose Show additional command output.
--version Print the Dart SDK version.
--enable-analytics Enable analytics.
--disable-analytics Disable analytics.

Available commands:
analyze Analyze Dart code in a directory.
compile Compile Dart to various formats.
create Create a new Dart project.
devtools Open DevTools (optionally connecting to an existing
application).
doc Generate API documentation for Dart projects.
fix Apply automated fixes to Dart source code.
format Idiomatically format Dart source code.
migrate Perform null safety migration on a project.
pub Work with packages.
run Run a Dart program.
test Run tests for a project.

Run "dart help <command>" for more information about a command.


See https://dart.dev/tools/dart-tool for detailed documentation.

Proseguiamo cercando di entrare nel dettaglio delle logiche


interne al linguaggio, affrontando il tema dell’esecuzione e della
compilazione.
Senza colpo ferire, prepariamo un file con cui iniziare subito a
giocare.

Listato 3.1 example.dart. ‘Esempio Hello World’


|void main(){
print('Hello World');
}

Esecuzione
In Dart abbiamo due possibilità di eseguire il codice: from
source, ovvero direttamente dal sorgente, e tramite snapshot. È
importante chiarire questo aspetto prima di procedere al
dettaglio della compilazione. Non sarà facile, ma ci proveremo:
mettetevi d’impegno.

From-source
L’esecuzione partendo dal codice sorgente avviene tramite
quella che viene chiamata compilazione JIT.
JIT
La Virtual Machine esegue codice Dart compilandolo just-in-
time. La compilazione JIT viene eseguita appena prima
dell’esecuzione del codice. Una spiegazione molto spicciola è
quella che la VM non compilerà tutto il codice dell’applicativo
alla sua esecuzione, ma si “limiterà” a compilare metodi e
funzioni che dovranno essere “appena” eseguite. Inoltre, questo
processo muta costantemente durante il runtime
dell’applicazione poiché il compilatore JIT implementa delle
particolari feature, tra le quali l’Adaptive optimization, che
consente di migliorare e ottimizzare le prestazioni durante il
runtime.
La compilazione JIT si pone a metà strada tra un approccio
interpretato e una compilazione “statica”. Un linguaggio
interpretato, a differenza di un software compilato in JIT, viene
eseguito del tutto a runtime, senza che venga, appunto,
compilato ed eseguito in codice macchina.
La compilazione JIT si avvale di un layer intermedio, chiamato
kernel. Questo è necessario in quanto la compilazione just-in-
time analizza innanzitutto il codice sorgente, creando un layer
intermedio in fase di compilazione. Successivamente, il runtime
della Dart VM, che riceve in pasto il file kernel, mantiene un link
a esso, per accedere a tutti i riferimenti che verranno
successivamente portati in memoria sulla Dart VM
(deserializzazione), nel momento in cui la sua esecuzione lo
necessita.
La compilazione JIT ha questa caratteristica: grazie al suo
runtime può accedere ai suoi dati di profilazione e ottimizzare il
più possibile i processi gestiti dal compilatore in fase di
esecuzione.
Figura 3.1 IL–intermediate language.

NOTA
Per codice macchina si intende un binario, direttamente eseguibile dal
device.

Ora immaginiamo l’esecuzione del sorgente durante il


processo di sviluppo. Il primo step si caratterizza dal nostro IDE
che esegue una build tramite il compilatore JIT della Dart VM.
La compilazione JIT, come abbiamo visto, rende incredibilmente
efficiente l’attività di sviluppo; ricordiamoci quanto sia utile la
feature hot reload nella fase di sviluppo.
Per comprendere ciò che avviene dietro le quinte aiutiamoci
con la Figura 3.2.

Figura 3.2 Esecuzione “from-source” con JIT.

La Dart VM, qualche versione fa, era in grado di eseguire


direttamente il codice sorgente. La versione di Dart 2.2 ha
introdotto un nuovo layer, il linguaggio kernel di cui abbiamo
appena parlato. La Dart VM, a oggi, consuma i file .dill. I file .dill
sono file binari, che contengono un linguaggio ad alto livello
chiamato Kernel AST, dove per AST si intende “Abstract syntax
tree”. Questo “linguaggio” si basa su quello che in italiano è
conosciuto come Albero sintattico astratto.
Un file AST è dunque una rappresentazione intermedia del
codice sorgente, con una struttura ad albero che include ogni
costrutto. L’introduzione di questa logica consente delle notevoli
ottimizzazioni e un design progettuale migliore: migliore sarà il
codice AST e più efficiente sarà il codice compilato generato.
Inoltre, i file AST possono anche essere utilizzati e scansionati
da tool per l’analisi statica del codice; ciò consente di effettuare
dei test senza effettivamente eseguire l’applicazione.
NOTA
Gli alberi sintattici astratti sono strutture dati ampiamente utilizzate dai
compilatori per rappresentare la struttura del codice sorgente. Fonte
Wikipedia.

Il file .dill è generato dal Dart Common Front End, chiamato


anche CFE. Questo componente, sviluppato in Dart e
chiaramente fornito dall’SDK, si occupa di “parsare” il codice
sorgente e creare il file kernel AST.
Questo processo è quello che viene eseguito nel nostro IDE
quando eseguiamo il Run di Flutter.
Dato un qualsiasi sorgente dart, questo può essere eseguito
da console.
$ dart run example.dart
Hello World

In questo caso vedremo direttamente l’output dell’esecuzione


del file .dart, ma dietro le quinte alla Dart VM verrà comunque
dato in pasto il file AST (.dill)
$ dart compile kernel example.dart
Compiling example.dart to kernel file example.dill.
Info: Compiling with sound null safety
$ dart run example.dill
Hello World

Snapshot
Snapshot è un’altra strada per eseguire il codice Dart.
Proseguiamo provando a capire che cosa si intende con questo
termine nel mondo Dart.
Il termine snapshot in italiano si traduce come “instantanea”.
Abbiamo appena visto che il kernel AST viene caricato dalla
Dart VM e consente di allocare nella memoria della Dart VM gli
oggetti in riferimento alle classi e le librerie contenuti nel kernel
AST. La gestione della memoria da parte della VM è delegata a
un componente chiamato Garbage Collector, che a sua volta è
contenuto in quello che viene definito runtime.
Nel caso di uno snapshot, al contrario di quanto appena visto,
il compilatore effettua una copia dell’heap (memoria Dart VM),
con un elenco di oggetti e altre informazioni, consentendo così
un avvio molto più veloce in quanto non è necessaria, come
nella compilazione JIT, l’analisi del codice.
Se il tema focale è l’esecuzione di un applicativo Dart, lo
snapshot è, a differenza di quanto visto nel precedente
paragrafo, ciò che consente la distribuzione dell’applicativo.
Infatti, l’uso di una snapshot è necessario nel caso in cui
dovessimo prepararci alla sua distribuzione.

JIT
Nel caso di una snapshot JIT, rispetto ad avere il sorgente,
l’applicativo può essere avviato partendo da un binario. La
snapshot JIT consente di eseguire il codice più velocemente,
rispetto che partendo dal sorgente, in quanto contiene già tutte
le classi e funzioni compilate necessarie all’esecuzione
dell’applicativo. La snapshot JIT è dipendente dall’architettura;
ciò non consente di distribuire la snapshot su un’architettura
differente rispetto a quella che ha creato la snapshot.
NOTA
In informatica, nella programmazione orientata agli oggetti,
deserializzare un oggetto significa ricostruirlo a partire dalla sua
rappresentazione binaria, ottenuta da un file o da un canale di rete. Il
processo inverso si chiama serializzazione e trasforma un oggetto
nella sua rappresentazione binaria che, successivamente, può essere
salvata su una memoria di massa o si può trasmettere su un canale di
rete (fonte Wikipedia).
$ dart compile jit-snapshot example.dart
Compiling example.dart to jit-snapshot file example.jit.
Info: Compiling with sound null safety
Hello World
$ dart run example.jit
Hello World

NOTA
Da notare l’esecuzione dell’applicativo in fase di compilazione JIT.
Ecco perché si fa riferimento al fatto che la snapshot JIT contenga
porzioni di codice già compilate.

Oltre alla snapshot JIT, vi è un’altra possibilità: AoT.

AoT
La compilazione JIT non è l’unica che offre il linguaggio Dart.
La compilazione AoT, a differenza di quella JIT, viene
precompilata del tutto. Il codice viene compilato in nativo dopo
l’analisi TFA (vedi Figura 3.3).
L’ulteriore step definito come Type Flow Analysis (TFA)
consente un’analisi statica del codice kernel AST con il fine
(consentitemi la superficialità) di eliminare le porzioni di codice
che non verranno utilizzate, implementando importanti
ottimizzazioni in fase di compilazione.
Il file binario AoT sarà sicuramente più “leggero” rispetto a
quello JIT, proprio perchè non è necessario includere altro oltre
il codice nativo. L’avvio di un applicativo compilato in AoT è
nettamente più veloce di quello JIT, proprio perché JIT, come
visto, si occupa di deserializzare le classi direttamente a
runtime, mentre in AoT questo processo è già eseguito in fase
di compilazione.

Figura 3.3 Compilazione AoT.

NOTA
Un aspetto peculiare è che JIT offre comunque prestazioni di picco più
elevate. Questo è possibile perché, ciclo su ciclo, il compilatore JIT,
grazie al profiling e alle informazioni sull’esecuzione del processo, è in
grado di ottimizzare il codice.

La compilazione AoT (ahead of time) avviene, come dice il


nome stesso, in anticipo. Il codice viene compilato direttamente
in codice macchina ARM o X64.
NOTA
ARM indica un’architettura di microprocessori, comunemente utilizzati
per device mobili, anche se ARM sta iniziando a prendere sempre più
piede anche su desktop e notebook. X86_64, invece, è notoriamente
utilizzata in ambienti Windows.

La compilazione AoT è mandatoria nell’utilizzo di Flutter per


una build di release, poiché il binario viene poi distribuito per le
varie piattaforme supportate e viene eseguita in questa
modalità.
È possibile distribuire una snapshot AoT, esattamente come
visto per JIT, con la differenza che la snapshot AoT non
contiene nessun runtime, pertanto è impossibile che venga
eseguita senza l’ausilio di un tool come dartaotruntime.
Le snapshot prodotte dipendono dall’architettura, esattamente
come le snapshot JIT, ma a differenza di queste ultime non
avendo runtime, sono molto più leggere.
Ricordiamoci che per le snapshot JIT troveremo già delle
porzioni di codice compilate, invece nelle snapshot AoT il codice
sarà del tutto trasformato in binario.
$ dart compile aot-snapshot example.dart
Info: Compiling with sound null safety
Generated: /Users/vgiacchina/work/dart/example.aot

Troveremo dartaotruntime dentro la directory /bin/cache/dart-


sdk/bin del nostro progetto Flutter. Provate a eseguirlo.

Self-contained executables
Questa compilazione genera un eseguibile per Windows,
macOS, o Linux. Il file in questione, per esempio un exe per
Windows, contiene, oltre al codice sorgente dell’applicativo, le
sue dipendenze e un ridotto runtime di Dart per gestire memoria
e garbage collector, insomma tutto il necessario per eseguire
l’applicazione. In gergo possiamo immaginare un self-contained
executables come un compilato AoT con un piccolo runtime. Il
termine runtime possiamo immaginarlo come una versione
minimale della Dart VM.
$ dart compile exe example.dart
Info: Compiling with sound null safety
Generated: /Users/vgiacchina/work/dart/example.exe
$ ./example.exe
Hello World

Dart JS
L’SDK Dart fornisce la possibilità di eseguire un’applicazione
Dart all’interno del browser. Inizialmente questo processo era
gestito da un tool separato, dart2js. Successivamente, questa
feature è stata direttamente integrata all’interno del tool dart,
tramite il comando dart compile js. Il codice Dart verrà appunto
trasformato in codice js, con la possibilità di scegliere diversi
livelli di ottimizzazione.

Mobile
Chiarito i concetti di base che ruotano intorno all’esecuzione
del codice Dart, negli step successivi eseguiremo del codice
all’interno dei nostri dispositivi mobili. La compilazione vera e
propria in codice macchina verrà gestita dal framework Flutter,
poiché verrà compilato il nostro codice Dart, oltre che l’SDK
Dart, ma anche del codice nativo per generare di fatto il nostro
apk o file ipa, rispettivamente Android e iOS.

Release
La compilazione per un progetto di release viene effettuata da
Flutter in AoT in libreria nativa, per esempio ARM. La libreria
conterrà sia il nostro codice Dart che l’SDK Dart. Per entrambe
le architetture, a loro volta, l’engine di Flutter verrà integrato
tramite Android NDK o LLVM per iOS in un progetto per creare
rispettivamente i file apk per Android e ipa per iOS.
Troverete i dettagli sulla compilazione AoT di Flutter al
seguente indirizzo:
https://github.com/flutter/flutter/wiki/Flutter-engine-operation-in-AOT-
Mode

Development
Il processo di sviluppo (debug mode), invece, viene eseguito
direttamente dalla Dart VM. Flutter rimane in attesa di
un’eventuale modifica del codice sorgente durante la sua
esecuzione; questa viene rilevata, viene generato un file kernel
e questo viene inviato al dispositivo mobile, che lo elabora e
riproduce sulla UI i relativi cambiamenti.
Il processo che abbiamo descritto è quello che viene definito
come snapshot kernel, che abbiamo già introdotto in
precedenza.
Concludendo questa parte, che ammetto essere piuttosto
ostica, possiamo immaginare che, per la parte di sviluppo,
l’applicativo porti con sé il compilatore JIT, gli strumenti di debug
e il runtime, composto dal garbage collector. Invece, per la parte
di release l’applicazione conterrà solo il runtime; proprio per
questo motivo vi è questa differenza tra le dimensioni di due
applicativi compilati nei due differenti modi.

Flutter
Avendo appreso la teoria che si cela dietro l’uso di Dart in
Flutter, proseguiamo con una breve introduzione di quella che è
l’architettura di alto livello di Flutter. Fatto ciò, prometto che
inizieremo a vedere del codice.

Architettura
L’architettura Flutter è composta da 3 strutture che devono
essere viste “verticalmente”:
Framework;
Engine;
Embedder.

Figura 3.4 Flutter architecture (fonte https://docs.flutter.dev/).


Framework
Il framework è la parte più in alto della pila ed è l’interfaccia
verso lo sviluppatore. Il framework è scritto in Dart e tramite Dart
si interagisce con il framework, che offre tutti gli strumenti per lo
sviluppo di applicazioni in Flutter.
La struttura interna al Framework si compone dall’involucro
esterno, composto da Material e Cupertino, le due librerie che
consentono di implementare i design, rispettivamente per
Android e iOS. Il layer Widgets rappresenta tutti gli elementi
messi in campo per la creazione della user interface della nostra
app.
Il layer rendering trasforma i nostri widget in pixel ed è di
fondamentale importanza in quanto costruisce un albero di
oggetti renderizzati. Foundation non è altro che la libreria Core
che fornisce le primitive.
NOTA
Il termine primitiva si riferisce a una funzione o classe di base offerta
framework, nel nostro caso dal Core Flutter framework primitives.

Engine
Il layer Engine è lo strato intermedio, il cuore C++ di Flutter.
Questo strato è definito engine proprio perché compone il
motore di Flutter. L’engine lavora a runtime e fornisce tutte le
API di basso livello: le interfacce per la rete e per l’I/O, le
animazioni, l’architettura per i plugin e tutta la toolchain Dart per
la compilazione e l’esecuzione.

Embedder
L’embedder è il punto di contatto tra Flutter e il sistema
operativo; questo layer sarà specifico per piattaforma.
L’embedder è scritto in Java e C++ per Android, Obejct-C/C++
per iOS e macOS e infine C++ per Linux e Windows.
La coerenza della UI in ambienti cross-platform è un punto di
forza di Flutter ed è gestita esattamente in questo layer. Il layer
Embedder dialoga con le API offerte del sistema operativo per il
calcolo della superficie del rendering. Oltre a questo l’embedder
è responsabile della gestione degli eventi (Event Loop), la
gestione dei thread, l’accessibilità, insomma tutta l’interazione
reale dell’applicativo con il device.
NOTA
Flutter è uno strumento così potente da rendere possibile la creazione
di un embedded specifico (layer embedder) che consentirebbe di
eseguire un’applicazione Flutter su una nuova piattaforma
(https://github.com/flutter/flutter/wiki/Custom-Flutter-Engine-
Embedders).

Reactive
Flutter può essere definito “reattivo”, poiché la sua architettura
consente questa particolarità. Affronteremo nel capitolo
successivo i widget, scopriremo che cosa sono e come
svilupparli. I widget compongono la nostra UI e una UI reattiva
consente di ragionare per componenti e porzioni specifiche della
user interface che compone il layout di un’applicazione. Questo
approccio garantisce un’efficienza migliore in termini di
progettazione e prestazioni.
Come vedremo, Flutter manterrà separata la logica di
business e i relativi dati dell’interfaccia dell’applicazione; questo
grazie al concetto di widget e poi di albero, che verrà
approfondito più avanti. Il widget è un contenitore di una
configurazione, la descrizione di un componente immutabile, e
Flutter riflette le modifiche sulla UI nel modo più puntuale
possibile.
Ciò che vedremo si basa sull’uso di componenti (widget), che
definiremo con una configurazione, in una porzione specifica
(rapporto padre-figlio) e Flutter si occuperà di utilizzare e
riutilizzare i widget a runtime, ottimizzando il più possibile i
processi di building e rendering.
Il nostro codice non gestirà realmente la modifica
dell’interfaccia, ma piuttosto notificheremo un cambiamento che,
grazie a un mapping tra “widget” e la sua reale implementazione
all’interno dell’engine di Flutter, consentirà all’interfaccia di
essere modificata da Flutter stesso nel modo più efficiente
possibile.
Questo è il concetto di reactive user interface.
Non preoccupatevi, tutto prenderà maggiore chiarezza una
volta approcciato lo sviluppo e il ciclo di vita di un’applicazione
Flutter.
Capitolo 4

Linguaggio

La doverosa premessa, prima di addentrarci nei meandri di


Dart, è che in questo capitolo tratteremo e porteremo degli
esempi “base”, in modo da dare al lettore un’infarinatura
generale per riuscire a comprendere più facilmente tutto il
codice che scriveremo, o almeno buona parte di esso. Allo
stesso tempo è indubbio che non potremo scavare troppo a
fondo, pertanto chiedo perdono in anticipo.
Chi ha già esperienza con la programmazione, soprattutto se
a oggetti, troverà semplice questo capitolo, poiché riporteremo
concetti già noti e applicati ad altri linguaggi. Chi approccia per
la prima volta la programmazione a oggetti, invece, farà
obbligatoriamente più fatica nell’apprendere le basi del
linguaggio. Proverò a introdurre i concetti fondamentali, ma
approcciare da zero un linguaggio di programmazione senza
affrontarlo in un libro dedicato rimane chiaramente un
compromesso.
Vi affido alla documentazione ufficiale (https://dart.dev/guides)
sperando che possa essere utilizzata per colmare tutte le lacune
che incontrerete.

Aspetti peculiari
Gli aspetti peculiari di Dart, che vedremo in questo capitolo,
possono essere riassunti in queste feature.
Dart è un linguaggio open source.
Dart è un linguaggio platform independent.
Dart è un linguaggio compilato.
Dart supporta la programmazione concorrente.
Dart è object oriented ed “everything’s an object”.
Dart è fortemente tipizzato, ma allo stesso tempo può
gestire la tipizzazione dinamica. Inoltre Dart supporta i tipi
generici.
Dart supporta il null safety. In poche parole puoi gestire se
una variabile può o non può contenere un tipo null, ed
eventualmente anche gestirne un’eccezione.
Dart supporta top-level declaration
Dart è un linguaggio con una curva di apprendimento molto
interessante. È un linguaggio semplice da imparare; pertanto
non perdiamoci in chiacchiere.

Variabili
Le variabili sono contenitori di dati. I dati in Dart possono
essere di diversi tipi (lo vedremo nel prossimo paragrafo). La
dichiarazione delle variabili può contenere o meno la definizione
del tipo di dato che la variabile conterrà, ma per garantire un
approccio type-safe è assolutamente consigliato che venga
fatto. L’approccio type-safe esclude la possibilità di incappare in
un tipo di dato non gestito. Non preoccupatevi, l’installazione
dell’estensione Dart, effettuata in VS Code qualche capitolo fa,
vi garantisce in real time la notifica di errori e warning e vi
supporta nella gestione dei tipi.
keyword <tipo_di_dato> nome;

keyword identifica appunto la dichiarazione della variabile e


vediamo come questo può essere fatto con: var, dynamic, const,
final e late.

var
void main() {
var a = 'pippo';
}

La variabile a contiene il riferimento a una Stringa. L’uso di var


consente a Dart di definire il tipo di dato per noi, rendendo la
dichiarazione meno esplicita, ma del tutto uguale a:
void main() {
String a = 'pippo';
}

dynamic
L’uso della keyword dynamic introduce la possibilità di cambiare
la tipologia di dato durante l’esecuzione. Il controllo del tipo
dynamic verrà così demandato direttamente alla fase di
esecuzione (runtime). Superfluo dire che, là dove è possibile, è
meglio evitare l’uso di dynamic, in quanto è sempre bene sapere
in anticipo con che tipologia di dato andremo a lavorare.
void main() {
dynamic a = 'pippo';
a = 3;
}

const
La keyword in questione definisce un dato immutabile. Viene
definita in fase di compilazione e dopo la sua dichiarazione non
sarà possibile sovrascrivere il dato in questione.
void main() {
const a = 'pippo';
}

final
Il concetto di dato immutabile vale anche per final, con la
differenza che questo dato può essere dichiarato anche in fase
di esecuzione, oltre che in compilazione.

late
La keyword late introduce il concetto di Lazy initializing.
Quando una variabile late viene dichiarata, questa viene
inizializzata solo quando verrà realmente usata.
late String a;

void main() {
a = 'pippo';
print(a);

null safety
Dart ha il concetto di null safety. Questa feature garantisce
che non vi è la possibilità di accedere a una variabile che può
contenere null. Per questo motivo tutte le variabili sono not-
nullable di default.
void main() {
String a;
print(a);
}

VS Code ci avvertirà con questo messaggio: The non-nullable


local variable ‘a’ must be assigned before it can be used. Try
giving it an initializer expression, or ensure that it’s assigned on
every execution path.
Le variabili di tipo dynamic possono invece contenere null,
proprio perché il compilatore non effettuerà il controllo sul tipo di
dato dynamic. È possibile definire una variabile nullable con
l’operatore ?
String? a = null;

È bene tenere a mente che ogni qual volta che si dichiara una
variabile senza assegnare un valore (inizializzazione), il suo
valore di default sarà null.

Tipi di dati
I principali tipi di dati che possono essere definiti in Dart sono:
int, double, String, bool, List, Set, Map.

Numeri
Il tipo di dato Numbers può contenere due tipologie di
Numbers: gli int e i double. Gli int (Integer, intero in italiano)
contengono numeri interi e i double i numeri in virgola mobile.
void main() {
int num1 = 2;
double num2 = 1.5;
var num3 = 1.5;
print(num1 + num2 + num3);
}

L’esempio fa uso anche della keyword var che in questo caso


consente di definire in maniera implicita il tipo di dato. In questo
caso un double.
NOTA
È possibile eseguire molto più velocemente il codice Dart, rispetto a
quanto visto precedentemente, con il comando dart run sorgente.dart
o, ancora più velocemente, con dart sorgente.dart. Questa esecuzione,
come sappiamo, cela dietro le quinte una compilazione JIT.

Figura 4.1 run.

NOTA
Le keyword sono “parole” riservate al linguaggio che non possono
essere utilizzate in altro modo dallo sviluppatore. var è un esempio.

Debug
Un aspetto che ci consente di utilizzare gli strumenti di debug
offerti da Dart è l’uso dei breakpoint. Il breakpoint consente di
stoppare l’esecuzione del codice, durante le fasi di sviluppo, per
analizzarla; per esempio: analizzare variabili locali, parametri,
funzioni, condizioni, oggetti ecc. È uno strumento
incredibilmente utile a uno sviluppatore e vi accompagnerà
costantemente nel processo di sviluppo. VS Code può eseguire
un file .dart in modalità debug e, se avremo posizionato un
breakpoint, basterà attivare “un punto di interruzione” all’inizio
della riga che si vuole stoppare. Una volta interrotta
l’esecuzione, fermandosi con il cursore sulla riga in oggetto è
possibile accedere ai dettagli del runtime. Ad applicazione
interrotta viene mostrato un menu che consente diverse opzioni,
tra le quali quella di riprendere il flusso, la possibilità di
proseguire riga per riga (F10), oltre a quella di ispezionare
anche il corpo della funzione presente nel flusso (F11).

Assert
Assert è una keyword che consente di facilitare il debug del

flusso applicativo. Assert consente di intercettare un’eventuale


condizione non soddisfatta che interromperebbe il flusso con un
errore. Lavora solo in modalità debug.
void main() {
var num = 1;
assert(num != 1);
}

Figura 4.2 run con debug.


Figura 4.3 analisi breakpoint.

Stringhe
Il tipo di dato Stringa è definibile con la keyword String.
void main() {
var a = "Hello";
String b = "World";
print('$a $b');
print(a + ' ' + b);
}

$ dart example_3.dart
Hello World
Hello World

Come è possibile vedere dalla funzione print, è possibile


concatenare le stringhe con l’operatore + oppure usare le
espressioni (${expression}) all’interno di una stringa.
Possiamo anche richiamare metodi all’interno di una stringa in
questo modo:
print('length: ${a.length + b.length}'); // length: 10

NOTA
In Dart i commenti si possono esplicitare con i due caratteri // per la
singola riga o tutto quello che si trova tra i caratteri /* e */. In questo
modo il compilatore ignorerà del tutto quanto scritto come commento.
Booleani
Il dato booleano accetta solo due costanti: true e false.
void main() {
bool equal;
equal = 1==1;
print(equal);
}

$ dart example_4.dart
true

Liste
Le liste, meglio conosciute ai più anziani come array,
contengono una lista ordinata di oggetti (ricordiamoci che ogni
cosa è un oggetto in Dart).
void main() {
List num = [1, 2];
var words = ['uno', 'due'];
print('${num[0]} ${words[1]}');
}

$ dart example_5.dart
1 due

Come si evince dall’esempio, possiamo accedere


direttamente agli elementi usando un indice [indice]. Un’altra
caratteristica delle liste è l’uso dell’operatore ..., chiamato
spread.
void main() {
List a = [1, 2];
List b = [...a, 3];
print(b);
}

$ dart example_6.dart
[1, 2, 3]

Lo spread, come appena visto, consente di combinare due o


più liste.
Mappe
Il tipo Mappa, con keyword Map, definisce una mappa
chiave/valore. Le mappe ci consentono di iniziare a strutturare i
dati all’interno della nostra applicazione.
void main() {
var a = {
'uno': 1,
'due': 2
};

print(a);
print(a['uno']);

a = Map();
a['tre'] = 3;
a['quattro'] = 4;

print(a);
print(a['tre']);
}

Nel primo caso, la mappa è stata creata direttamente con


chiave valore; nel secondo caso, usando la keyword Map. In
quest’ultimo caso è necessario usare la sintassi Map(), in quanto
Map è una classe (vedremo in seguito nel dettaglio la
definizione di classe) e deve essere istanziata.
$ dart example_7.dart
{uno: 1, due: 2}
1
{tre: 3, quattro: 4}
3

Posizionando il cursore sulla variabile, possiamo vedere la


tipologia di mappa creata automaticamente. È possibile, anche
tramite Map, definire direttamente all’inizializzazione il tipo di dato
chiave/valore.
"a = Map<String, int>();"
Figura 4.4 Tipo di dato.

Come si evince dal codice, è possibile accedere agli elementi


della mappa tramite un indice, esattamente come visto per le
liste.

Set
I set sono una lista non ordinata di elementi. Possono essere
utilizzati per memorizzare una lista univoca di input.
void main() {
var list = {'uno', 'due', 'tre', 'tre', 'quattro'};
print(list);
}

$ dart example_8.dart
{uno, due, tre, quattro}

Come vediamo dall’output, la lista di elementi è univoca.


Avremmo potuto dichiarare la lista pure con la keyword Set.
Set list = {'uno', 'due', 'tre', 'tre', 'quattro'};
NOTA
List, Map e Set offrono una serie di properties e metodi che consentono
di interagire con essi facilmente. Metodi come add, addEntry, remove,
indexOf e map consentono di aggiungere, rimuovere, cercare, ciclare, i
dati con estrema facilità.

Generici
I generici sono una forma di definizione del dato che consente
di scrivere codice più sicuro. Prendiamo per esempio la
dichiarazione:
List a = [1, 2];

VS Code ci mostra, posizionando il cursore sopra il nome


della variabile, che viene specificato un dato per il tipo List,
ovvero List<dynamic>; ciò significa che i dati contenuti nella lista
sono di tipo dinamico. Basti vedere sulla documentazione
ufficiale di Dart la definizione di List per rendersi conto che il tipo
di dato List ha questa caratteristica: List<E> class. E sta per
elemento ed è semplicemente una convenzione.
Detto ciò, sarebbe stato più corretto l’uso dei generici:
List<int> a = [1, 2];

Questo concetto si estende a molti altri elementi, lo vedremo


più avanti.

Funzioni
Le funzioni sono dei costrutti che racchiudono delle istruzioni
per svolgere un determinato compito.
void main() {
String text = helloworld("Hello", "World");
print(text);
}
String helloworld(String str1, String str2){
return '$str1 $str2';
}

La funzione helloworld accetta due parametri e ritorna una


stringa separata da uno spazio.
$ dart example_9.dart
Hello World

È possibile definire dei parametri opzionali in due modi


differenti.
String helloworld(String str1, [String? str2])

L’uso delle [] definisce il parametro opzionale; in questo


specifico caso ci avvaliamo anche del null safety esplicitando
che il parametro str2 può essere null.
Possiamo inoltre usare le {} per gestire i parametri tramite i
nomi: questo ci consente di non rispettare l’ordine di definizione
dei parametri e di definirli come opzionali di default. Nel caso in
cui questi siano mandatori, è possibile usare la keyword required.
void main() {
String text = helloworld(str2:"World", "Hello");
print(text);
}

String helloworld(String str1, {String ?str2}) {


return '$str1 $str2';
}

$ dart example_10.dart
Hello World

main
La funzione a cui dare risalto, seguendo tutti gli esempi finora
trattati, è certamente la funzione main(). La funzione main è
indispensabile per qualsiasi applicazione scritta in Dart poiché è
il suo punto di ingresso, il nostro “entry point”.
È possibile passare dei parametri alla funzione main, utile per
esempio nello sviluppo di un batch o uno script da console.
void main(List a) {
print(a);
}

$ dart example_11.dart Hello World


[Hello, World]

Rispetto alla funzione helloworld definita come String, la


funzione main è definita come void. Questa tipologia di dato è
usata quando indichiamo che una funzione non ha nessun
valore da “ritornare”. Al termine dell’esecuzione della funzione
main corrisponderà la conclusione del programma.

Ricordiamo nuovamente che in Dart tutto è un oggetto e


grazie a questo è possibile anche usare le funzione come
parametri di altre funzioni. Facciamo un esempio.
void main(){
exec(helloworld);
}

void exec(Function f){


print(f());
}

String helloworld() {
return "Hello World";
}

$ dart example_12.dart
Hello World

In questo esempio abbiamo definito due funzioni: exec e


helloworld. Nella funzione main eseguiamo la funzione exec e
passiamo come parametro helloworld, che altro non farà che
ritornare una stringa. La funzione exec stamperà a video quanto
ritornato da f (helloworld), ovvero "Hello World".
Lambda
Scrivendo codice ci troveremo spesso a fare uso di funzioni
lambda, ovvero funzioni che non hanno un nome. Riprendiamo
l’esempio precedente, ma utilizzeremo l’approccio lambda.
void main(){
exec((){
return "Hello World";
});
}

void exec(Function f){


print(f());
}

Scope
Lo scope è la definizione della visibilità di una variabile
all’interno di una porzione di codice. Dart è un linguaggio
definito “lessicale” (lexical language) e la visibilità di una
variabile è facilmente ricavabile osservando se questa è definita
tra un blocco di parentesi graffe.
Un esempio banale, ma efficace.
main() {
String main = "Main scope";
void child() {
String levelOne = 'Second scope';

void subchild() {
String levelTwo = Third scope';
print('$main');
print('$levelOne');
print('$levelTwo');
}
subchild();
}
child();
}

$ dart example_14.dart
Main scope
Second scope
Third scope
Lo scope della variabile main è riferita a tutto il blocco che vive
all’interno della funzione main, infatti questa è visibile anche dalla
funzione subchild. Diversamente la funzione levelTwo vive solo
all’interno della funzione subchild. Se provassimo a inserire un
print('$levelTwo'); fuori dalla funzione subchild, il compilatore ci
avvertirebbe con un: Error: Undefined name 'levelTwo'.

Closure
Il termine closure indica una funzione che mantiene accesso
al suo scope anche se richiamata fuori dal suo contesto.
Function addOne() {
int count = 1;
return () {
count = count + 1;
print(count);
};
}

void main() {
f = addOne();
f();
f();
}

$ dart example_15.dart
2
3

In questo esempio utilizziamo per la prima volta una variabile


alla quale associamo una funzione, tant’è vero che la funzione
addOne è definita come Function, proprio perché ritorna una

funzione.
La funzione interna a addOne accede tramite scope alla
variabile count e si premura, a ogni esecuzione, di aumentare
count di uno. Nonostante l’esecuzione di addOne termini, la
funzione interna mantiene la sua referenza alla variabile count e
questo ci consente di incrementare il suo valore anche al di fuori
di addOne. Questo approccio è definito come lexical closure.

Arrow syntax
Arrow syntax è una sintassi compatta molto usata. In pratica
consente di utilizzare il costrutto => per le funzioni che sono
composte da una sola istruzione, e ciò consente di risparmiare
qualche linea di codice. Facciamo un esempio.
void main() {
final int a = sum(1, 1);
print(a);
}

int sum(int a, int b) {


return a + b;
}

La funzione sum con la sintassi arrow può essere riscritta in


questo modo:
int sum(int a, int b) => a + b;

Classi
La classe è un concetto fondamentale della programmazione
orientata agli oggetti. Come ripetuto molte volte, “everything is
an object”: ogni oggetto ha un suo modello che lo definisce.
Questo modello è una classe. L’oggetto altro non è che l’istanza
di una classe.
class Book {
String? title;
int pages = 0;
bool read = false;
static String catalog = "Marketing";

Book(String title, int pages){


this.title = title;
this.pages = pages;
}
void desc(){
print('Title: ${this.title} with ${this.pages} pages editor by
$catalog');
}

void changeCatalog(String newCatalog){


catalog = newCatalog;
}

void main(){
var o = new Book("Dart", 200);
o.read = true;
o.desc();
o.changeCatalog("Development");
print(Book.catalog);
}

Class Book definisce il modello dell’oggetto Book. La keyword


new nella funzione main ci consente di istanziare la classe Book
(l’uso di new non è più mandatorio ed è possibile ometterlo).
L’oggetto o ci consentirà così di accedere a membri e metodi.
I membri di una classe sono le variabili, mentre i metodi sono
le funzioni. Per intenderci, title pages e read sono membri (o
properties o fields), desc è un metodo. Tramite l’oggetto o è
possibile accedere al modello. VS Code, così come tanti altri
IDE, offre una feature chiamata IntelliSense che consente il
completamento del codice.

Figura 4.5 VS Code IntelliSense.


Nella Figura 4.5 vediamo come l’IDE ci suggerisce membri e
metodi. L’operatore . ci consente di accedere, tramite l’oggetto,
al suo modello.

Costruttore
Il costruttore è una funzione speciale che viene eseguita in
fase di inizializzazione della classe. Nel nostro caso il
costruttore Book ci consente di valorizzare i membri tramite
parametri passati al costruttore. Il costruttore non è mandatorio
e non ha nessun valore di ritorno.
Book(String title, int pages){
this.title = title;
this.pages = pages;
}

È possibile definire più costrutti; prendiamo in esempio questo


sorgente.
void main() {
Book b1 = new Book();
Book b2 = new Book.withTitle('Dart');

print(b1.title);
print(b2.title);
}

class Book {
String? title;
Book() {
print("undefined book");
}
Book.withTitle(String title) {
this.title = title;
}
}

$ dart example_17.dart
undefined book
null
Dart

This
Un’altra novità introdotta dal sorgente appena mostrato è
l’operatore this.
Book(String title, int pages){
this.title = title;
this.pages = pages;
}

In questo caso avremmo potuto fare riferimento al membro


title senza l’uso del this, ma come avremmo potuto distinguere

il title parametro dal title membro della classe? In questo caso


ci viene in aiuto this, che ci consente di fare sempre riferimento
all’istanza stessa.

Static
Nell’esempio introdotto con il paragrafo Class abbiamo anche
introdotto la keyword static. Le variabili definite static vengono
referenziate a livello di classe, infatti è possibile accedervi
direttamente dalla classe e non dall’oggetto; di conseguenza
tutte le istanze di Book faranno riferimento unico a questo
“catalog”.

Getter/Setter
Get e Set sono metodi speciali che consentono di applicare
della logica nel leggere o scrivere dati all’interno della classe.
Nell’esempio fornito possiamo vedere come applicare una
logica nella fase di scrittura del membro title.
void main() {
Book b = new Book();
b.book_title = "Flutter";
print(b.book_title);
}
class Book {
String? title;
String? get book_title {
return title;
}

void set book_title(title) {


if(title == "Dart"){
this.title = title;
}
}
}

In questo caso il membro title non verrà valorizzato, poiché


abbiamo definito una condizione nel setter che non verrà
soddisfatta.

Ereditarietà
L’ereditarietà è un concetto portante della programmazione a
oggetti. Per ereditarietà si intende la possibilità di definire una
classe ereditando tutti gli attributi di una classe madre. Nella
fattispecie, Dart supporta l’ereditarietà singola, ovvero non può
ereditare caratteristiche da più classi.
void main() {
DevBook b = new DevBook();
b.catalog();
b.devCatalog();
}

class Book {
void catalog() {
print('Book catalog');
}
}

class DevBook extends Book{


void devCatalog() {
print('DevBook catalog');
}
}

La keyword extends ci consente di “estendere” la classe Book;


così facendo la classe DevBook ereditarietà il metodo
devCatalog.
$ dart example_18.dart
Book catalog
DevBook catalog$
Nel caso in cui le classi abbiano dei costruttori, questi
verranno eseguiti nell’ordine: madre, figlio. L’esecuzione del
costruttore di una classe ereditata senza l’uso della keyword
super è definito implicito.

void main() {
DevBook b = new DevBook();
}

class Book {
void catalog() {
print('Book catalog');
}
Book(){
print("Book costructor!");
}
}

class DevBook extends Book{


DevBook(){
print("DevBook costructor!");
}
void devCatalog() {
print('DevBook catalog');
}
}

$ dart example_19.dart
Book costructor!
DevBook costructor!

Questa logica cambia nel caso in cui i costruttori debbano


gestire dei parametri. In questo caso è necessario usare la
keyword super e questo approccio è definito esplicito. La
keyword in questione è utilizzata dalla classe figlia per
richiamare quella che sarebbe la superclasse, in pratica la
classe madre. Può essere utilizzato per invocare il costrutto,
membri e metodi della superclasse.
void main() {
DevBook b = new DevBook("Dart");
b.devCatalog();
}

class Book {
String? title;
void catalog() {
print('Book catalog');
}
Book(String title){
this.title = title;
print("Book constructor with title $title");
}
}

class DevBook extends Book{


String? devTitle;
DevBook(String devTitle): super(devTitle){
print("DevBook constructor with title $devTitle");
}
void devCatalog() {
super.catalog();
print('DevBook catalog');
}
}

In questo esempio abbiamo ereditato il costruttore della


superclasse ed esteso il metodo della classe figlia con quello
della superclasse.
$ dart example_19.dart
Book constructor with title Dart
DevBook constructor with title Dart
Book catalog
DevBook catalog

Override
Se volessimo sovrascrivere il metodo della classe madre
dovremmo usare l’annotation override. Le annotazioni sono
strumenti che consentono di informare il compilatore di una
peculiarità. Nel nostro caso l’uso dell’annotation @override,
appena prima di un metodo, informa il compilatore (e lo
sviluppatore, migliorando così anche la manutenibilità del
codice) che stiamo sovrascrivendo il metodo della classe
madre. È possibile in Dart usare @override per metodi, membri e
getter/setter.
void main() {
DevBook b = new DevBook();
b.catalog();
}

class Book {
void catalog() {
print('Book catalog');
}
}

class DevBook extends Book{


@override
void catalog() {
print('new Book catalog');
}
}

$ dart example_19.dart
new Book catalog

Interfacce
Le interfacce definiscono il modello di una classe in maniera
astratta, descrivono i metodi della classe e i membri (escluso il
costruttore). L’interfaccia conterrà solo la definizione del
metodo, poi il corpo del metodo sarà implementato nella classe
che utilizza l’interfaccia.
Il suo uso è tipico della programmazione a oggetti ed è
presente in moltissimi linguaggi. In Dart le interfacce sono
implicite, ovvero nascono direttamente quando definiamo la
nostra classe. Al momento in cui creiamo una classe, dietro le
quinte possiamo immaginare la creazione speculare della sua
interfaccia.
Proviamo a chiarire il concetto con un esempio.
class Book {
String? title;
void catalog() {
print('Book catalog');
}
}

class DevBook implements Book{


String? title;
void catalog() {
print('Book catalog');
}
void devCatalog() {
print('DevBook catalog');
}
}

void main() {}

Tramite l’uso della keyword implements è possibile


implementare l’interfaccia Book per la classe DevBook.
Il sorgente in oggetto riesce a compilare senza errori proprio
perché la classe DevBook implementa esattamente i membri e i
metodi presenti nella classe Book. L’invito che vi faccio è di
modificare qualcosa che non riproduca esattamente quanto
presente nell’interfaccia Book per ottenere un errore in fase di
compilazione. Questo approccio è usato proprio per offrire una
specifica e una costrizione allo sviluppo di classi.
NOTA
È possibile applicare il concetto di eredità multipla delle interfacce
molto semplicemente elencando una lista di interfacce. Per esempio:
“implements Book, Book1, Book2”.

Abstract
Le classi abstract vengono usate per rendere più manutenibile
ed efficiente la struttura del codice. Questo concetto è tradotto,
nel linguaggio della programmazione a oggetti, come
“astrazione”. In altre parole si astraggono delle classi, che
rappresentano funzionalità specifiche prive di implementazione,
con il fine di frammentare il design del progetto in parti più
piccole e circoscritte. Immaginiamo di avere una classe già in
uso che vogliamo riscrivere, ma che rimanga “compatibile” con il
resto del codice. Se avessimo una classe che astrae i metodi,
utilizzeremmo quella per “rifattorizzare” la classe e questa
rimarrebbe “compatibile”.
abstract class Book {
void catalog();
}

class DevBook extends Book {


void catalog() {
print("DevBook catalog");
}
}
class MathBook extends Book {
void catalog() {
print("MathBook catalog");
}
}
void main() {
Book b = new DevBook();
MathBook m = new MathBook();

b.catalog();
m.catalog();
}

$ dart example_21.dart
DevBook catalog
MathBook catalog

NOTA
Il refactoring o rifattorizzazione è una pratica dell’ingegneria del
software che prevede una modifica importante alla struttura del codice
senza però modificarne il comportamento.

Mixin
Come già anticipato poco sopra, Dart non ha l’ereditarietà
multipla. Per intenderci, Dart potrebbe ereditare i metodi di una
classe provando a estendere le classi a cascata. Facciamo un
esempio banale:
class Test {
var t1 = 0;
}
class Test1 extends Test {
var t2 = 1;
}
class Test2 extends Test1{
var t3 = 2;
}
void main(){
Test2 t = Test2();
print('${t.t1} ${t.t2} ${t.t3}');
}

$ dart example_22.dart
0 1 2

In questo esempio, visto la possibilità di ereditare dalla classe


madre tutti gli attributi, a cascata la classe Test2 ha
effettivamente accesso ai membri delle due classi Test e Test1.
Una struttura di questo tipo andrebbe realizzata
implementando l’ereditarietà multipla tramite la keyword mixin.
abstract class Book {
void catalog();
}

mixin MathBook {
void mathCatalog() {
print("MathBook catalog");
}
}

mixin BioBook {
void bioCatalog() {
print("BioBook catalog");
}
}

class DevBook extends Book with MathBook, BioBook{


void devCatalog() {
print("DevBook catalog");
}
void catalog(){
print("Book catalog");
}
}

void main() {
DevBook b = DevBook();
b.catalog();
b.bioCatalog();
b.devCatalog();
b.mathCatalog();
}

$ dart example_23.dart
Book catalog
BioBook catalog
DevBook catalog
MathBook catalog

In questo esempio abbiamo strutturato il codice


implementando, con un certo grado di astrazione, la possibilità
di estendere più classi contemporaneamente. Esempi di questo
tipo sono alla base della programmazione a oggetti in
applicazioni di medie e grosse entità. Il limite dell’ereditarietà
singola di Dart in questo modo viene arginato.

Istruzioni di controllo
Come in qualsiasi linguaggio conosciuto dall’uomo, i principali
“control instruction” sono presenti anche in Dart. Vediamo
superficialmente come vengono approcciati.
If/else rappresentano le istruzioni condizionali

if(1==0) {
//
} else if(1==2) {
//
} else {
//
}

Il loop è un ciclo iterativo; questo può essere eseguito tramite


le keyword for e while/do-while.
for(int a=0;a<4;a++){
print(a); // 0,1,2,3
}

La keyword while valuta la condizione prima dell’esecuzione, il


costrutto do-while dopo l’esecuzione.
int a =0;
while(a<4){
print(a); // 0,1,2,3
a++;
}

do {
print(a); // 4
} while (a>4);
Il costrutto switch-case si basa su un confronto e una serie di
blocchi che ne identificano la scelta.
int a = 4;
switch(a){
case 3:
print('3');
break;
case 4:
print('4'); //blocco eseguito
break;
default:
print('-1');
}

Nei loop è possibile gestire l’interruzione e la possibilità di


saltare una interazione tramite le keywords continue e break;
for(int a=0;a<4;a++){
if(a==0) {
continue;
} else if(a==1) {
continue;
} else {
break;
}
print(a); // non sarà mai eseguito
}

Eccezioni
La gestione delle eccezioni è una pratica comune in tutti i
linguaggi di programmazione. Le eccezioni sono eventi anomali
che alterano il flusso del programma e Dart offre il costrutto
try/catch per gestirle.

Dart offre una lista di eccezioni:


IntegerDivisionByZeroException

IOException

DeferredLoadException

FormatException
IsolateSpawnException

Timeout

Ognuna di queste eccezioni descrive una casistica precisa.


Procediamo con un esempio.
void main() {
try {
int result = 20 ~/ 0;
print('result:$result');
} catch (e) {
print('Exception:$e');
}
}

$ dart example_24.dart
Exception:IntegerDivisionByZeroException

In questo esempio facciamo ricorso al costrutto try/catch. La


keyword try definisce un blocco che può generare un’eccezione,
mentre catch catturerà l’eccezione “genericamente”. Se fossimo
a conoscenza dell’eccezione in cui potremmo incappare nel
nostro blocco di codice, saremmo in grado di gestire l’errore
specifico.
try {
int result = 20 ~/ 0;
} on IntegerDivisionByZeroException{
print('IntegerDivisionByZeroException');
}

Oltre a on, abbiamo un'altra feature offerta da Dart nella gestione


delle eccezioni: la keyword finally. Il blocco contrassegnato da finally
verrà sempre eseguito, eccezione o meno.

void main() {
try {
int result = 20 ~/ 0;
print('result:$result');
} catch (e) {
print('Exception:$e');
} finally {
print('finally');
}
}

$ dart example_25.dart
Exception:IntegerDivisionByZeroException
finally

Finally è usato in genere per liberare le risorse. Immaginiamo


un flusso in cui abbiamo necessità di concludere la lettura di un
file, piuttosto che terminare una connessione a un database.
Vista l’interruzione del flusso a causa dell’eccezione, questo
potrà essere fatto tramite finally.

Moduli
Il codice Dart può essere modularizzato. L’approccio alla
programmazione modulare ci consente di strutturare il codice
con maggiore attenzione, rendendo così le nostre applicazioni
più manutenibili. In Dart questo concetto è ancor di più
esasperato in quanto “every Dart app is a library”. In genere
chiamiamo modulo una routine che, “importata” nel nostro
codice, introduce una nuova feature; la libreria può essere
immaginata come una raccolta di moduli.
Le librerie in Dart si importano con l’uso della keyword import e
possono essere importate come librerie “core” di Dart, facendo
direttamente uso del prefisso dart: o tramite il prefisso package:,
se effettivamente stiamo importando un package, oppure
puntando direttamente al file Dart solo con il suo path.
NOTA
Un package in Dart è una libreria, o anche un tool, che può essere
distribuito grazie al suo package manager.
import 'dart:math';

void main() {
print(Random().nextInt(6));
}

$ dart example_26.dart
5
$ dart example_26.dart
2

In questa sorgente vediamo come, grazie all’import della


libreria math, possiamo fare uso della classe Random. Il metodo
nextInt restituisce un intero randomico tra 0 e 6.
Ora facciamo un esempio con una libreria custom.
Listato 4.1 library.dart
void helloworld() {
print("Hello World");
}

Listato 4.2 example_27.dart


import 'library.dart';

void main() {
helloworld();
}

$ dart example_27.dart
Hello World

Avremmo anche potuto utilizzare la keyword as per fare


riferimento alla libreria.
Per esempio import 'library.dart' as h e h.helloword().

Iterable
Gli iterable sono collezioni di valori a cui è possibile accedere
in modo sequenziale. Gli iterable sono alla base di altre strutture
dati, per esempio List o Map. La loro struttura è simile al tipo di
dato List, ma con la differenza che iterable non consente l’uso di
un indice tramite []. Iterable implementa anche metodi che
consentono di interagire con gli elementi, come first, last, e
elementAt. Il primo metodo consente di accedere al primo
elemento dell’Iterable, last l’ultimo e infine elementAt accede
tramite un indice.
Facciamo un esempio.
void main() {
Iterable<int> i = [0, 1, 2];
print(i.first);
print(i.last);
print(i.elementAt(0));
for (var n in i) {
print(n);
}
}

Iterable implementa anche un iteratore che consente di


accedere sequenzialmente agli elementi, esattamente come il
costrutto for-in.
void main() {
Iterable<int> i = [0, 1, 2];
Iterator itr = i.iterator;
while (itr.moveNext()) {
print(itr.current);
}
}

Operazioni asincrone
Dart offre la possibilità di eseguire, e di conseguenza gestire,
il risultato di operazioni asincrone. Le operazioni asincrone sono
routine che possono essere gestite parallelamente al flusso di
esecuzione del codice. Possiamo immaginare una casistica in
cui una funzione dovrà recuperare dei dati dalla lettura di un file,
piuttosto che da una chiamata HTTP, e allo stesso momento
lasciare libero l’utente di proseguire nell’uso dell’applicativo fino
alla notifica dell’avvenuta ricezione del dato.
In Dart, l’asincronicità è gestita fondamentalmente attraverso
due strumenti: Future e Stream.
Procediamo con un esempio piuttosto immediato.
void main() {
Future.delayed(Duration(seconds: 2), () {
print('2');
});
print('1');
}

$ dart example_28.dart
1
2

La class Future offre il metodo delayed, che permette di


eseguire una callback al termine del tempo definito con la
classe Duration, in questo caso due secondi.
NOTA
In programmazione, una callback o richiamo è generalmente una
funzione o un “blocco di codice” che viene passato come parametro a
un’altra funzione (fonte Wikipedia).

Come possiamo vedere dalla sua esecuzione, l’output 1


precede 2, nonostante l’ordine delle istruzioni suggerisca altro.
Ciò accade poiché la Classe Future consente di gestire le
operazioni asincrone, consentendo al codice di proseguire nella
sua elaborazione mentre l’operazione asincrona viene eseguita.
L’istanza della classe Future gestisce fondamentalmente due
stati: uncompleted e completed. Lo stato uncompleted identifica l’arco
temporale dell’esecuzione dell’operazione asincrona, lo stato
completed identifica il termine dell’operazione.

Future
Il costruttore Future è istanziabile con una funzione, nella quale
potremo gestire le nostre routine asincrone.
Future<String> f = Future(() {
return 'Hello World';
});
Then
L’istanza Future ci offre il metodo then per accedere al risultato
dell’operazione asincrona, che corrisponde all’esito della Future.
void main() {
helloWorld().then((value) {
print('Future value: $value');
});
}

Future<String> helloWorld() {
return Future<String>.delayed(Duration(seconds: 2), () {
return 'Hello World';
});
}

$ dart example_29.dart
Future value: Hello World

In questo esempio, ciò che abbiamo fatto è ritornare una


Future attraverso la funzione helloWorld. Questa verrà richiamata
a sua volta all’interno del nostro main e, tramite il metodo then,
avremo modo, una volta terminata la Future ed eseguita la
callback definita dalla Future, di accedere alla stringa contenuta
nell’argomento value.

Async/Await
Rispetto a quanto visto, un’altra possibilità è l’uso delle
keyword: async/await. La differenza rispetto all’uso del metodo
then consiste nel fatto che con async/await il flusso del codice
viene interrotto fino al completamento della Future.
void main() async {
String value = await helloWorld();
print('Future value: $value');
}

Future<String> helloWorld() {
return Future<String>.delayed(Duration(seconds: 2), () {
return 'Hello World';
});
}
Errore
La gestione degli errori dovrà essere gestita tramite il
costrutto try/catch
void main() async {
try {
String value = await helloWorld();
print('Future value: $value');
} catch (e) {
print('Future error: $e');
}
}

Future<String> helloWorld() {
return Future<String>.delayed(Duration(seconds: 2), () {
throw 'Error';
});
}

$ dart example_31.dart
Future error: Error

La stessa logica si applica all’uso del metodo then. Questo


può essere gestito tramite callback e tramite l’uso del metodo
CatchError.

void main() {
helloWorld().then((value) {
print('Future value: $value');
}, onError: (e) {
print(e);
});
}

Future<String> helloWorld() {
return Future<String>.delayed(Duration(seconds: 2), () {
throw 'Error';
});
}

$ dart example_32.dart
Error

void main() {
helloWorld().then((value) {
print('Future value: $value');
}).catchError((e) {
print('$e');
});
}

Future<String> helloWorld() {
return Future<String>.delayed(Duration(seconds: 2), () {
throw 'Error';
});
}

$ dart example_33.dart
Error

whenComplete
Esattamente come l’uso del metodo finally nel costrutto
try/catch, whenComplete esegue lo stesso compito.
Indipendentemente dal successo o dal fallimento della Future,
whenComplete sarà comunque eseguita.

helloWorld().then((value) {
print('Future value: $value');
}).catchError((e) {
print('$e');
}).whenComplete(() {
print('complete!');
});
}

$ dart example_34.dart
Future value: Hello World
complete!

Stream
La classe stream consente di gestire un flusso di eventi
asincroni. La gestione della comunicazione è piuttosto semplice.
L’emittente emette un dato e questo verrà ricevuto se ci sarà un
listener in ascolto.
Facciamo subito un esempio pratico.
import 'dart:async';

Stream helloWorld() {
return Stream.periodic(const Duration(seconds: 1), (index) {
return 'Hello World';
});
}

Stream singleHelloWorld() async* {


yield 'Hello';
await Future.delayed(Duration(seconds: 2));
yield 'World';
}

void main() {
Stream h = helloWorld();
h.listen((event) {
print('Stream value $event');
});
Stream s = singleHelloWorld();
s.listen((event) {
print('Stream single value $event');
});
}

Nell’esempio appena proposto troviamo due nuove keyword:


async* e yield. La prima è il corrispettivo della keyword async, ma

specifica per gli Stream. Una funzione contrassegnata da async*


dovrà tornare uno Stream e il dato verrà emesso tramite la
keyword yield. Esattamente come accade nella funzione
singleHelloWorld. Lo stream consente di sottoscrivere uno o più
soggetti, tramite l’uso del metodo listen; questo consente di
accedere al valore emesso nello stream.
Oltre a yield è possibile anche utilizzare la keyword yield*,
proprio per non farci mancare nulla. Questa keyword è usata
per implementare flussi ricorsivi. Facciamo un esempio.
import 'dart:async';

Stream incrementValue(int n) async* {


await Future.delayed(Duration(seconds: 1));
yield n++;
if (n < 3) yield* incrementValue(n);
}

void main() {
Stream s = incrementValue(0);
s.listen((event) {
print('Stream value $event');
});
}

StreamController
La classe StreamController è per definizione un controller di
Stream. StreamController rende meno ostica la gestione dei flussi:
consente di gestirne i dati ed eventuali errori. È anche possibile
accedere con facilità ad altre informazioni relative allo Stream,
per esempio i sottoscrittori.
import 'dart:async';

void main() {
int a = 0;
final streamController = StreamController<int>();
Timer.periodic(Duration(seconds: 1), (timer) {
if (a > 3) {
streamController.close();
timer.cancel();
return;
}
streamController.add(a++);
});

streamController.stream.listen(
(event) {
print(event);
},
onDone: () => print('Done'),
onError: (error) => print(error),
);
}

$ dart example_35.dart
0
1
2
3
Done

NOTA
La classe StreamSubscription definisce una sottoscrizione al flusso
Stream, ed è esattamente l’istanza ritornata dal metodo listen.
Il primo passo è quello di importare la libreria async, per
supportare la classe streamController. La classe in questione
consente di creare uno stream e gestire il suo flusso. In questo
esempio faccio uso di Timer.periodic che consente di invocare
una callback ogni secondo.
All’interno del timer inviamo, a ogni invocazione della
callback, un dato (a). Facciamo anche in modo di chiudere lo
streaming dopo 3 secondi. La chiusura dello streaming è
definita dal metodo close.
Dall’altra parte del tubo, resteremo in attesa del dato tramite il
metodo listen dell’oggetto stream a cui potremo accedere grazie
al controller. Il metodo listen gestisce tre callback differenti. La
prima viene invocata per accedere al dato ricevuto dallo stream.
A seguire troviamo: onDone e onError. In ordine, la prima callback
scatta quando lo stream viene chiuso tramite il metodo close, il
secondo quando il dato è inviato tramite il metodo addError.

Broadcast
L’esempio precedente ha una tipologia di stream 1:1. Ciò
vorrebbe dire che, se avessimo provato a metterci in ascolto
due volte sullo stesso Stream, avremmo ottenuto un errore.
Esiste la possibilità di inviare un dato in broadcast. Il termine
broadcast sta a significare che l’emissione dello stesso dato
verrà inviato a più destinatari.
Procediamo con un esempio modificando il precedente.
import 'dart:async';

void main() {
int a = 0;
final streamController = StreamController<int>.broadcast();
Timer.periodic(Duration(seconds: 1), (timer) {
if (a > 3) {
streamController.close();
timer.cancel();
return;
}
streamController.add(a++);
});

streamController.stream.listen(
(event) {
print('listen One: $event');
},
onDone: () => print('listen One:Done'),
onError: (error) => print(error),
);
streamController.stream.listen(
(event) {
print('listen Two: $event');
},
onDone: () => print('liste Two:Done'),
onError: (error) => print(error),
);
}

$ dart example_36.dart
listen One: 0
listen Two: 0
listen One: 1
listen Two: 1
listen One: 2
listen Two: 2
listen One: 3
listen Two: 3
listen One:Done
liste Two:Done

await for
Esiste un’alternativa all’uso di listen per l’elaborazione dei dati
all’interno di uno Stream. Questa alternativa è l’uso del costrutto
await for.

Procediamo con un esempio.


import 'dart:async';

void main() async {


int a = 0;
final streamController = StreamController<int>();
Timer.periodic(Duration(seconds: 1), (timer) {
if (a > 3) {
streamController.close();
timer.cancel();
return;
}
streamController.add(a++);
});

await for (final value in streamController.stream) {


print(value);
}

print('stream closed');
}

$ dart example_37.dart
0
1
2
3
stream closed

In questo esempio catturiamo subito la differenza rispetto


all’uso di listener. La differenza principale dovrebbe saltare
all’occhio proprio grazie a quanto già visto precedentemente
con l’uso di await. Sostanzialmente l’esecuzione del codice verrà
interrotta fino alla ricezione di un dato dallo stream; con l’uso del
metodo listener questo non accadrebbe.
Capitolo 5

Widget

Nel precedente capitolo abbiamo visto un po’ di teoria. Ora


che abbiamo un’infarinatura sul linguaggio Dart, siamo pronti a
scrivere del codice.
NOTA
Tutti i sorgenti che troverete in questo volume sono reperibili
all’indirizzo https://github.com/vgflutter/flutter_apogeo.

Apriamo senza indugi il progetto hello_world (creato


nell’omonimo capitolo).

Listato 5.1 main.dart


import 'package:flutter/material.dart';

void main() {
runApp(const MyApp());
}

class MyApp extends StatelessWidget {


const MyApp({super.key});

@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: const MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}

class MyHomePage extends StatefulWidget {


const MyHomePage({super.key, required this.title});
final String title;

@override
State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {


int _counter = 0;

void _incrementCounter() {
setState(() {
_counter++;
});
}

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
const Text(
'You have pushed the button this many times:',
),
Text(
'$_counter',
style: Theme.of(context).textTheme.headlineMedium,
),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: _incrementCounter,
tooltip: 'Increment',
child: const Icon(Icons.add),
),
);
}
}

Procediamo con l’analisi di questo file: la nostra prima


applicazione.
Come prima cosa importiamo il package material.dart che ci
fornirà i widget che implementano il material design.
import 'package:flutter/material.dart';
Ogni applicazione Dart che sia eseguibile necessita di una
funzione main, e questo esempio non fa eccezione.
void main() {
runApp(const MyApp());
}

runApp è una funzione offerta dalla libreria widget. runApp


rende radice il widget passato come parametro, nel nostro caso
MyApp.
NOTA
Se il device che state usando esegue l’applicazione sovrapponendola
a elementi del sistema operativo, per esempio la barra di navigazione,
è bene usare il widget SafeArea che gestirà automaticamente lo
spazio dell’app rispetto al sistema operativo. SafeArea si rende
necessario soprattutto quando non viene usato il widget Scaffold con
un AppBar.

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

@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: const MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}

La classe MyApp estende la classe StatelessWidget che,


come suggerisce il nome, non gestisce “uno stato”. Al momento
immaginiamola come un componente statico della user
interface, mentre possiamo pensare a uno stato come un luogo
dove memorizzare dei dati. Abbiate pazienza, vedremo tutto tra
poco. Procediamo intanto a un’analisi superficiale.
La classe MyApp definisce un costruttore che fa uso di una
key. L’uso di una key è una buona pratica poiché aiuta il
framework Flutter a identificare univocamente un widget tra tutti
gli altri. Non è sempre necessario utilizzarlo, ma non accade
niente di brutto se lo si utilizza sempre. Come vedrete anche
l’analisi del codice di VS Code vi suggerirà di inserirlo.

Super initializer
La definizione di questo costrutto potrebbe confondervi in
quanto nel capitolo di Dart non abbiamo trovato una sintassi di
questo tipo. Questa sintassi è definita “super initializer” ed è una
abbreviazione introdotta dalla versione 2.17 di Dart.
La sintassi const MyApp({super.key}) è un’abbreviazione di:
const MyApp({Key key}) : super(key: key)

Se ancora non fosse chiaro, facciamo un ulteriore passo


indietro:
class Book {
final String? title;
Book({this.title});
}

class DevBook extends Book {


DevBook({super.title});
}

Con la versione 2.17, Dart ha semplicemente semplificato il


modo di passare gli argomenti al super costruttore.

Build
Infine, proseguiamo con l’override di build. Build è un metodo
(StatelessWidget class) che si occupa di definire l’oggetto
Widget. In questo caso definiamo il widget MaterialApp con le
chiavi title, theme, primarySwatch e home, rispettivamente il
titolo, il tema complessivo dell’app definito tramite la classe
ThemeData, il materialColor primarySwatch e il widget
MyHomePage.
NOTA
Il tema viene definito per tutto l’applicativo tramite la key theme in
MaterialApp; ciò non toglie che questo comportamento possa essere
ridefinito per una porzione di albero specifica usando il widget Theme,
che avvolgerà il widget a cui si vuole configurare un tema differente.

La property home darà il via alla costruzione di una struttura


verticale di widget (chiamata anche albero dei widget o, in
inglese, widget tree), di cui MyHomePage è in cima.
Esattamente come definito dal codice, il metodo build dovrà
ritornare un widget. La semantica utilizzata nella costruzione
della nostra app sarà caratterizzata dall’utilizzo di questa logica
di sviluppo: definire la build di widget che potrà avvolgere uno o
più widget figli. Nel nostro caso il primo Flutter widget usato è
MaterialApp.

Material design
Il Material design è un design creato da Google che dal 2015
ha iniziato a caratterizzare tutte le principali applicazioni del
parco Google, fino a introdurre, con Android 12 (parliamo del
2021), un nuovo rifacimento del Material design che caratterizza
tutto il sistema operativo Android. Nonostante Google abbia
creato il Material design parecchi anni fa per offrire una
coerenza di UI e UX in tutto il parco Google, app e web, solo di
recente è stato fatto un ulteriore e fondamentale passo avanti
verso un’evoluzione che mette al centro di tutto la
personalizzazione dell’esperienza utente, ovvero il Material You,
chiamato anche Material 3.
Flutter ha già iniziato a migrare i widget verso Material 3 e la
sua introduzione in Flutter è già gradualmente in atto, proprio
per essere il più indolore possibile. A oggi è possibile, dove
concesso, forzare l’uso del nuovo design con la property
useMaterial3 presente in ThemeData. Nei nostri sorgenti non ne
faremo uso, ma vi invito a usare questa property proprio per
vedere le differenze attraverso i widget.
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
useMaterial3: true,
),
home: const MyHomePage(title: 'Flutter Demo Home Page'),
);

Il Material design è composto da un’infinità di componenti:


bottoni, text input, selectors, bars, cards, insomma tutto ciò che
compone una UI. In aggiunta, il Material design personalizza i
componenti con colori, forme e animazioni accattivanti. Se
appartenete al mondo Google, probabilmente, senza saperlo,
siete già esperti utilizzatori del Material design.
Potete ottenere una panoramica dell’offerta del nuovo design
navigando il sito: https://m3.material.io/. La versione corrente è
Material 2: https://m2.material.io/design.
Potete serenamente utilizzare il Material design per tutte le
piattaforme supportate. Qualora questo non dovesse piacervi,
poiché desiderate che la vostra app iOS abbia un design stile
Apple, Flutter offre una serie di widget che adottano il design
Cupertino. Il Widget CupertinoApp è il corrispettivo di
MaterialApp, ed è possibile usufruire di un cospicuo parco
widget in stile Apple.
MyHomePage widget
La home, in gergo chiamata anche come default root, è stata
definita con MyHomePage.
class MyHomePage extends StatefulWidget {
const MyHomePage({super.key, required this.title});

final String title;

@override
State<MyHomePage> createState() => _MyHomePageState();
}

La classe MyHomePage estende StatefulWidget. La classe


StatefulWidget, a differenza di StatelessWidget, gestisce uno
stato. Al momento, possiamo immaginare lo stato come un
cassetto in cui conservare delle informazioni. Lo stato
consentirà al widget di renderlo dinamico, offrendo la possibilità
all’utente di interagire con esso e di conseguenza generare dati
che avranno vita all’interno dell’interfaccia utente. Quanto
descritto è l’esatto opposto di quanto avviene con il widget
StatelessWidget.
Anche questo widget viene istanziato con la key e un titolo. Il
passo successivo è l’override del metodo createState che crea
uno stato tramite la classe MyHomePageState.
A differenza del metodo build, i widget StatefulWidget
necessitano della creazione di uno stato. Il processo di build
sarà poi definito all’interno dello stato.
NOTA
L’uso dell’underscore rende privato un elemento: funzioni, campi e
metodi di una classe, se dichiarati con il parametro _, risulteranno
invisibili oltre al file .dart d’origine.

class _MyHomePageState extends State<MyHomePage> {


int _counter = 0;

void _incrementCounter() {
setState(() {
_counter++;
});
}

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
const Text(
'You have pushed the button this many times:',
),
Text(
'$_counter',
style: Theme.of(context).textTheme.headlineMedium,
),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: _incrementCounter,
tooltip: 'Increment',
child: const Icon(Icons.add),
),
);
}
}

La classe _MyHomePageState estende la classe State.


Esattamente come la classe MyApp, anche in questo caso
abbiamo il metodo build, in cui viene definito e ritornato il widget
Scaffold.
Scaffold è un widget che implementa la struttura base del
Material design, infatti la sua traduzione in italiano è
“impalcatura”.
I parametri che definiscono Scaffold sono:
appBar, una toolbar con un titolo;
body, un widget che verrà posizionato sotto la toolbar;
floatingActionButton, un bottone float posizionato
nell’angolo in basso (bottom) a destra.
Al widget floatingActionButton viene associata una classe
FloatingActionButton, che ci consente di definire delle callback per

interagire con il bottone. Nella fattispecie, al “pressed” del


bottone viene lanciato il metodo _incrementCounter, che incrementa
counter, il quale a sua volta viene mostrato nel body da un
widget figlio. Esattamente il dato counter è quello che necessita
del concetto di stato.
Prima di terminare l’analisi del codice, un ultimo chiarimento è
legato all’uso di const. Spesso troveremo l’uso di const associato
al costruttore di un widget; questo è utile poiché, quando
definiamo un widget come const, Flutter saprà che non ha senso
ricostruirlo, in quanto per definizione è una costante in fase di
compilazione, che non cambierà mai nel tempo. Durante il ciclo
di vita dell’applicazione, la UI sarà dinamica e i widget dovranno
essere ricostruiti, ma un widget const non lo sarà mai. L’uso di
const, là dove è possibile, rende più efficiente l’applicazione.
In questa brevissima analisi abbiamo visto molto velocemente
il codice del nostro hello_world, ma tantissimi concetti devono
essere chiaramente spiegati, uno tra tutti il “widget”.
Figura 5.1 Widget tree hello_world.

L’albero in Figura 5.1 rappresenta ciò che abbiamo visto


strutturato nel codice. In Flutter ogni cosa è un widget, pertanto
tutto quello che faremo sarà principalmente strutturare il layout
della nostra app per ospitare i nostri widget. I widget vengono
annidati: partendo dalla nostra radice (MyApp), abbiamo widget
che mostrano del testo, widget che definiscono la posizione di
altri widget nello spazio e widget con i quali è possibile
interagire. Abbiamo ripetuto la parola widget troppe volte senza
ancora averla introdotta come si deve: ora è il caso di farlo.

Widget
Flutter offre widget per coprire ogni aspetto che la vostra app
vorrà offrire. Negli esempi che vedremo, come già affrontato in
hello_world, troveremo widget che estendono due tipologie di
classi: Stateless e Stateful. Oltre a queste categorie, ci sono
anche widget legati strettamente al layout che sono detti widget
invisibili, semplicemente perché non renderizzano elementi
grafici tangibili. In Figura 5.1, nell’esempio precedente, Column
è un widget di questo tipo.

StatelessWidget
I widget che non modificano il loro stato perché rimangono
statici nel corso dell’esecuzione dell’app sono definiti stateless.
import 'package:flutter/material.dart';

void main() {
runApp(
const Center(
child: Text(
'Hello World',
textDirection: TextDirection.rtl
),
),
);
}

Modifichiamo il file main.dart del nostro hello_world, utilizzando


il widget Text. L’esempio creato è piuttosto eloquente, oltre a
essere estremamente minimale.
runApp creerà una struttura verticale di widget, di cui il primo
è il widget Center. A sua volta il widget Center consente di
definire un widget figlio: il widget Text. Text è un widget stateless
poiché, come è facile intuire, l’utente non può interagire con un
testo e questo non cambierà mai.
I più svegli, alla definizione di widget stateless associata al
widget Text, si staranno chiedendo perché venga definito
stateless un widget che, nell’esempio iniziale fornito da Flutter,
al suo interno contiene una variabile che effettivamente cambia
il testo mostrato. La spiegazione è semplice: in primis il widget
Text non crea interazione con l’utente; in secundis il widget
verrà ricompilato (esattamente ciò che accade quando l’utente
interagisce con il bottone FloatingActionButton, vedi esempio
hello_world).
I widget stateless sono statici, ma ciò non significa che non
possano cambiare a fronte di una compilazione. Inoltre,
nell’esempio troviamo due widget Text:
children: <Widget>[
const Text(
'You have pushed the button this many times:',
),
Text(
'$_counter',
style: Theme.of(context).textTheme.headlineMedium,
),
],

Il primo è definito const proprio perchè non avrebbe senso


ricompilarlo; nel caso del secondo, invece, una ricompilazione
sarà necessaria per mostrare un testo differente.
Concludiamo il paragrafo cambiando l’approccio: invece che
creare un widget tree “inline”, definiamo una classe.
import 'package:flutter/material.dart';

void main() {
runApp(const MyApp());
}

class MyApp extends StatelessWidget {


const MyApp({super.key});

@override
Widget build(BuildContext context) {
return const Center(
child: Text(
'Hello World',
textDirection: TextDirection.ltr
),
);
}
}
L’esempio appena creato fa uso del metodo BuildContext, che
è un aspetto fondamentale dello sviluppo di applicazioni in
Flutter.

BuildContext
Il metodo build ha come argomento BuildContext. Il metodo
build crea un nuovo nodo all’interno del widget tree e allo stesso
tempo BuildContext inietta nella funzione builder ciò che viene
chiamato context. Il context può essere immaginato come un
ponte di accesso dal widget al widget tree.
import 'package:flutter/material.dart';

void main() {
runApp(const MyApp());
}

class MyApp extends StatelessWidget {


const MyApp({super.key});

@override
Widget build(BuildContext context) {
return Container(
color: Colors.amber,
child: const Text("Hello World", textDirection: TextDirection.ltr)
);
}
}

In questo esempio la struttura ad albero è quella raffigurata in


Figura 5.2.
L’albero rappresentato in Figura 5.2 chiarisce la struttura. La
struttura ad albero è gestita tramite l’handler BuildContext.
Il metodo build restituisce un widget e ogni widget ha il suo
BuildContext. L’utilizzo del BuildContext sarà sempre più chiaro
man mano che si procede con lo sviluppo. Verranno affrontate
diverse casistiche, in cui all’interno di una funzione di building
sarà necessario accedere al contesto ereditato. Avremo
casistiche in cui dovremo sfruttare questa feature, per esempio
l’uso della classe Theme o la navigazione, che utilizza il
contesto per navigare tra le view. Vedremo nel breve futuro
diversi esempi di questo tipo. L’importante al momento è chiarire
che il contesto è esattamente lo strumento che ci consente di
interagire con l’albero dei widget.

Figura 5.2 Widget tree.

Un esempio pratico e semplice è quello in cui un widget


accede, tramite il context, a un’informazione definita nel widget
MaterialApp.
import 'package:flutter/material.dart';

void main() {
runApp(const MyApp());
}

class MyApp extends StatelessWidget {


const MyApp({super.key});

@override
Widget build(BuildContext context) {
return MaterialApp(
theme: ThemeData(primaryColor: Colors.lightBlue[800]),
home: Scaffold(
body: Center(
child: Text(
"Hello World",
textDirection: TextDirection.ltr,
style: TextStyle(color: Theme.of(context).primaryColor)
)
)
)
);
}
}

Il sorgente in questione fa utilizzo del metodo of, disponibile


per alcuni widget. Il metodo of è una sorta di “instance locator”,
ed è disponibile per alcune classi. In genere viene utilizzato per
localizzare e accedere a widget o, più in genere, a oggetti
tramite il BuildContext, esattamente come avviene per
Theme.of. In questo caso utilizziamo Theme.of per accedere
all’istanza ThemeData, utilizzata nel MaterialApp widget per definire
il tema dell’app.

StatefulWidget
L’interattività in un’applicazione è un aspetto di vitale
importanza. Rispetto ai widget stateless, che sono statici, i
widget Stateful sono dinamici e consentono di gestire i
cambiamenti dell’interfaccia che possono avvenire, per
esempio, sia per l’interazione dell’utente con la User Interface
che per eventi asincroni. Il widget in sé stesso rimane
comunque immutabile; ciò che invece cambia è lo stato
associato al widget.
Il widget gestisce la sua dinamicità attraverso l’uso degli stati.
Prendiamo come esempio la classe MyHomePage del solito
hello_world:
class MyHomePage extends StatefulWidget {
const MyHomePage({super.key, required this.title});

final String title;

@override
State<MyHomePage> createState() => _MyHomePageState();
}

In questo sorgente la classe viene istanziata tramite il suo


costruttore che, oltre alla key, gestisce anche la stringa “title”. La
property title è definita come final in quanto il widget, definito
come const, obbliga le sue property a essere esplicitate come
final, proprio perché il valore della stringa sarà immutabile.
Potreste anche non usare const e final, ma il vostro IDE vi
avvertirà che non è una scelta corretta, proprio per il concetto di
immutabilità che ripeteremo fino allo svenimento.

State
Infine l’override è l’aspetto peculiare dei widget stateful. Il
corrispettivo del metodo build per questa tipologia di widget è il
metodo createState. Questo metodo crea uno stato mutabile, a
differenza del widget, a cui viene associata l’istanza del widget.
In genere ogni nuova istanza del widget genera un nuovo
stato. Esistono delle possibilità che vi consentono di condividere
lo stato tra più istanze tramite una special key, ma lo vedremo
qualche capitolo più avanti.
Alla creazione dello stato viene associata la classe
_MyHomePageState che si occuperà di gestire lo stato.

class _MyHomePageState extends State<MyHomePage> {


int _counter = 0;
void _incrementCounter() {
setState(() {
_counter++;
});
}

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
const Text(
'You have pushed the button this many times:',
),
Text(
'$_counter',
style: Theme.of(context).textTheme.headlineMedium,
),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: _incrementCounter,
tooltip: 'Increment',
child: const Icon(Icons.add),
),
);
}
}

Lo stato gestirà la variabile _counter che è inizializzata con


valore 0. Il metodo _incrementCounter esegue setState dopo aver
incrementato il counter. Il metodo setState notifica al framework
che c’è un cambiamento in canna e il framework si occuperà di
ricompilare il widget con la finalità di renderizzare la modifica
sullo screen, in questo caso l’oggetto _counter che ha cambiato
valore. Lo state ha il suo metodo build e il relativo BuildContext,
esattamente come i widget stateless.
NOTA
Nel corso del libro faremo riferimento con il termine stato o state allo
stesso concetto, ovvero lo stato definito per il widget Stateful.

In questo esempio usiamo anche la variabile widget. La


variabile widget può essere usata dallo state per accedere alla
classe del widget corrispondente, per richiamare properties o
metodi definiti appunto nella classe che estende StatefulWidget.

Lifecycle
Il ciclo di vita dei widget è differente tra widget stateless o
stateful. Il lifecycle di un widget stateless è piuttosto semplice
(vedi Figura 5.3): abbiamo la classe e il suo processo di build
che restituisce un widget.
Il widget stateful ha un lifecycle più complesso. La gestione
dello stato ha un suo ciclo di vita ben definito. Proviamo ad
analizzare insieme la Figura 4. Il widget, ha il suo costruttore
che definisce un nuovo stato. La creazione di un nuovo stato
mette in campo un processo ben definito. Il primo passo è la
valorizzazione di mounted, una property della casse State. Se la
property è settata a true, il widget risulterà “montato”, ovvero
presente nel widget tree. Il passo successivo è l’esecuzione del
metodo initState, questo viene eseguito appena dopo la
creazione dello state. initState può essere utilizzato, ad
esempio, per gestire eventi attraverso dei listener, vedremo più
avanti qualche caso d’uso.
initState verrà eseguito una sola volta, dopo la creazione del
widget. Proseguendo didChangeDependencies verrà eseguito
quando cambia una dipendenza del widget, ad esempio un
widget genitore.
Figura 5.3 Lifecycle stateless widget.

Il processo che vede “dirty state” al centro del lifecycle


corrisponde a uno stato di disallineamento tra il widget e il
framework; è in atto un cambiamento all’interno dello stato del
widget, ma il framework non ha ancora aggiornato il processo di
rendering.
Il cambiamento del dirty state avviene contestualmente
all’invocazione del metodo setState, che appunto viene usato al
cambio di un oggetto o dall’esecuzione del metodo
didUpdateWidget.
didUpdateWidget viene sovrascritto quando si vuole
intercettare la ricostruzione del widget e cogliere l’opportunità
per definire qualche comportamento, come per esempio l’uso di
un’animazione particolare.
Quando il dirty state viene aggiornato viene eseguito il
processo di build. Dopo la build, il processo ricomincia in quanto
quello che viene definito come clean, verrà nuovamente
“sporcato” con l’interazione dell’utente con i nostri stateful
widget.
Infine, il metodo dispose termina il ciclo di vita dello stato del
widget. Può essere usato per liberare le risorse, prima che il
widget venga eliminato dall’albero. Il metodo dispose elimina
definitivamente il widget; il metodo deactive, invece, viene
eseguito quando il widget viene eliminato dall’albero. Questa
operazione, però, potrebbe essere temporanea, in quanto il
widget potrebbe essere eliminato e ricreato da un’altra parte
dell’albero con lo stesso state. Una volta eliminato il widget
dall’albero, la property mounted verrà settata a false.
Figura 5.4 Lifecycle stateless widget.

Il lifecycle ha un impatto sul codice sotto forma di metodi che


abbiamo necessità di sovrascrivere con l’uso dell’annotazione
override. Analizziamo in dettaglio i metodi che costituiscono il
lifecycle con l’ausilio di una piccola modifica al nostro fido
hello_world.

Listato 5.2 main.dart


import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}

class MyApp extends StatelessWidget {


const MyApp({super.key});

@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: const MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}

class MyHomePage extends StatefulWidget {


const MyHomePage({super.key, required this.title});

final String title;

@override
State<MyHomePage> createState() {
debugPrint('Parent createState!');
return _MyHomePageState();
}
}

class _MyHomePageState extends State<MyHomePage> {


int _counter = 0;

void _incrementCounter() {
setState(() {
debugPrint('Parent setState!');
_counter++;
});
}

@override
void initState() {
debugPrint('Parent initState!');
super.initState();
}

@override
void didChangeDependencies() {
debugPrint('Parent didChangeDependencies!');
super.didChangeDependencies();
}

@override
Widget build(BuildContext context) {
debugPrint('Parent build!');
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
const Text(
'You have pushed the button this many times:',
),
Text(
'$_counter',
style: Theme.of(context).textTheme.headlineMedium,
),
ElevatedButton(
style: ElevatedButton.styleFrom(textStyle: const
TextStyle(fontSize: 12)),
onPressed: _incrementCounter,
child: const Text('Increment'),
),
const ChildHomePage(),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: () =>
Navigator.of(context).push(MaterialPageRoute(builder: (context) => const
NewPage())),
tooltip: 'Increment',
child: const Text('New'),
),
);
}
}

class ChildHomePage extends StatefulWidget {


const ChildHomePage({super.key});
@override
State<ChildHomePage> createState() => _ChildHomePage();
}

class _ChildHomePage extends State<ChildHomePage> {

@override
void didUpdateWidget(ChildHomePage oldWidget) {
debugPrint('ChildHomePage didUpdateWidget!');
super.didUpdateWidget(oldWidget);
}

@override
Widget build(BuildContext context) {
return const Padding(
padding: EdgeInsets.all(10),
child: Text('I\'m a child')
);
}
}

class NewPage extends StatefulWidget {


const NewPage({super.key});
@override
State<NewPage> createState() {
debugPrint('NewPage createState!');
return _NewPageState();
}
}
class _NewPageState extends State<NewPage> {
@override
void dispose() {
debugPrint('NewPage disposed!');
super.dispose();
}

@override
void deactivate() {
debugPrint('NewPage deactivate!');
super.deactivate();
}

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('New Page'),
),
body: const Center(
child: Text('This is the new page')),
);
}
}

NOTA
Il widget Padding consente di inserire il padding, ovvero dello spazio
vuoto tra il widget ed il suo bordo.

createState
Il metodo createState è invocato una singola volta per la
creazione di uno State. Potrebbe essere invocata più volte se lo
stesso widget è presente più volte nel nostro widget tree. Al
contrario, un widget potrebbe essere eliminato dal widget tree e
ricreato successivamente.
Nell’esempio che abbiamo appena fornito, possiamo vedere
che il metodo createState è invocato una volta per il widget
MyHomePage e N volte per il widget NewPage. Senza entrare
adesso nel dettaglio del widget Navigator, possiamo pensarlo
come uno strumento che ci consente di navigare verso altre
pagine che compongono la nostra applicazione. In questo il
widget verrà creato e rimosso definitivamente dall’albero ogni
volta.
NOTA
Come potete notare VS Code, tramite un warning, sconsiglia di
introdurre logica applicativa nel metodo createState. È sempre buona
norma seguire i consigli che Dart fornisce tramite l’analisi del codice
che effettua. La configurazione dell’analisi può essere cambiata
personalizzando la configurazione definita nel file analysis_options.yaml.

initState
Il metodo initState viene invocato esattamente dopo il metodo
createState, appena il widget viene inserito nel widget tree. La
logica segue quella di createState: N volte verrà creato un
oggetto State, N volte verrà invocato questo metodo. L’override
di initState, (ciò significa voler applicare una logica in questo
punto del lifecycle) tornerebbe utile per esempio per
sottoscrivere un listener. Se questo gergo vi suona nuovo,
possiamo immaginare il listener come un gestore di eventi. Il
metodo initState nella vita dell’istanza del widget è invocato una
sola volta, a differenza di build, e questa è una differenza
importante per talune implementazioni.
@override
void initState() {
debugPrint('Parent initState!'); // logica da introdurre
super.initState();
}

didChangeDependencies
Il metodo didChangeDependencies viene invocato subito
dopo il metodo initState. Oltre a questa invocazione,
didChangeDependencies consente di intercettare modifiche a
una dipendenza del widget. Il metodo didChangeDependencies
è usato per fare quelle operazioni che al rebuild del widget
sarebbe troppo dispendioso da ripetere N volte. In poche parole,
cambiando una dipendenza il widget verrà comunque
ricompilato, ma didChangeDependencies identifica comunque
una modifica a una dipendenza. Il fatto che comunque il widget
verrebbe ugualmente ricompilato rende questo metodo non così
usato.
NOTA
La dipendenza a cui facciamo riferimento può essere immaginata
come un legame tra oggetti differenti. Nel caso di Flutter quindi, un
widget che può accedere a risorse condivise con altri widget, per
esempio un Provider (lo introdurremo più avanti).

build
Il metodo build viene eseguito dopo il metodo initState o dopo
didUpdateWidget. Inoltre un widget viene ricompilato quando viene

invocato il metodo setState o una dipendenza dello state


cambia.

didUpdateWidget
Il metodo didUpdateWidget viene eseguito quando le
proprietà di un widget parent cambiano. Esattamente come
nell’esempio fornito, possiamo vedere che il metodo
didUpdateWidget dello stato _ChildHomePage viene invocato
quando lo stato del widget genitore MyHomePage effettua un
cambiamento sulla property _counter e di conseguenza esegue
un setState per aggiornare il widget.
class _MyHomePageState extends State<MyHomePage> {
int _counter = 0;

void _incrementCounter() {
setState(() {
debugPrint('Parent setState!');
_counter++;
});
}

@override
void didUpdateWidget(ChildHomePage oldWidget) {
debugPrint('ChildHomePage didUpdateWidget!');
super.didUpdateWidget(oldWidget);
}

La possibilità che offre il metodo di confrontare l’attuale stato


del widget con il suo stato precedente ci consente di introdurre
delle logiche, come un’eventuale animazione. È un classico uso
del metodo didUpdateWidget.

dispose
Il metodo dispose viene invocato quando un widget viene
eliminato dal widget tree. Nell’esempio fornito, quando torniamo
nella “Home” il framework invocherà il suddetto metodo. È utile
posizionarsi in questo punto per poter liberare le risorse.
@override
void dispose() {
debugPrint('NewPage disposed!');
super.dispose();
}
deactivate/activate
Il metodo deactivate viene invocato prima di dispose, ma, a
differenza di dispose che è “definitivo”, deactivate può essere
usato, insieme ad activate, in situazioni in cui il framework
piuttosto che eliminare il widget dall’albero, lo rimuove per
reinserirlo in un nuovo punto dell’albero. Lo stesso principio si
applica al metodo activate.
Come già anticipato, questo tema sarà più chiaro quanto il
concetto di key verrà approfondito.

Widget Base
Lo scopo di questa sezione è introdurre un numero ristretto di
widget per poter iniziare a creare delle piccole applicazioni per
giocare con il widget tree. Nonostante io mantenga un approccio
“discorsivo”, tengo molto che venga compresa la logica del
framework, e spero che il messaggio venga compreso il più
possibile. Nonostante si possano realizzare intere applicazioni
senza aver appreso a fondo le nozioni legate al funzionamento
interno del framework, vi garantisco che nel medio lungo
periodo risulta davvero utile.
I widget base che ci aiuteranno ad approcciare le nostre app
sono certamente quelli che consentono di strutturare il nostro
layout e sperimentare creando delle semplici viste.

Text
Abbiamo già introdotto il widget Text negli esempi forniti.
Innanzitutto, prendiamo la buona abitudine, nel momento in cui
approcciamo un nuovo widget, di prendere in esame il suo
costrutto, e facciamo sempre riferimento alla documentazione
ufficiale (https://api.flutter.dev/flutter/widgets/Text-class.html). È
possibile visionare il costrutto e una breve descrizione anche
tramite l’IDE, posizionandosi con il cursore sul nome della
classe.
Text(String data, {Key? key, TextStyle? style, StrutStyle? strutStyle,
TextAlign? textAlign, TextDirection? textDirection, Locale? locale,
bool? softWrap, TextOverflow? overflow, double? textScaleFactor, int?
maxLines, String? semanticsLabel, TextWidthBasis? textWidthBasis,
TextHeightBehavior? textHeightBehavior, Color? selectionColor})

Un altro passaggio, a cui dedicheremo un ampio spazio più


avanti, è l’ereditarietà del widget in questione, che è sempre
presente in documentazione.
Object -> DiagnosticableTree -> Widget -> StatelessWidget -> Text

Dall’ereditarietà si evince che il widget sia di tipo Stateless.


Per i più curiosi DiagnosticableTree è una classe interna al
framework che, tra le altre cose, si occupa della gestione delle
stringhe.
Il widget Text consente di stampare a video una stringa. Nulla
ci vieta di suddividere una stringa in più widget Text.
Nel nostro amato hello_word, troviamo questo esempio.
const Text
('You have pushed the button this many times:',),

Text
('$_counter',style: Theme.of(context).textTheme.headlineMedium,),

In questi due esempi, vediamo due costruttori differenti. Nel


primo caso l’oggetto viene creato solo con una stringa, nel
secondo caso viene definito uno stile tramite la property
opzionale style.
Nel secondo caso la property style ha come dato TextStyle.
La classe TextStyle ci aiuta a definire lo stile della stringa che
verrà stampata. Nel nostro caso, lo stile applicato sarà catturato
tramite la classe Theme.
Theme.of(context).textTheme.headlineMedium consente di catturare la
property headlineMedium della classe textTheme, tramite il
metodo of. In questo caso con il metodo of, offerto da Theme, è
possibile accedere alle proprietà del tema default offerto da
MaterialApp.
Nell’esempio fornito, infatti, la classe MyApp usa il widget
MaterialApp.
class MyApp extends StatelessWidget {
const MyApp({super.key});

@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
home: const MyHomePage(title: 'Flutter Demo Home Page'),
);
}

La property headlineMedium è definita nel thema di default,


che a sua volta una specifica del material design:
https://m2.material.io/design/typography/the-type-system.html#type-

scale.

NOTA
Rispetto ad altri esempi già visti, dove è stato necessario definire una
textDirection per il widget Text, in questo non non se ne fa uso poiché
la textDirection rtl è già una proprietà di default definita da
MaterialApp.

Row
Il widget Row consente di allineare i figli orizzontalmente. Il
widget Row spesso ricorre al widget Expanded per, appunto
espandere, il testo (e di conseguenza la riga) per tutto lo spazio
disponibile.
Row({Key? key, MainAxisAlignment mainAxisAlignment =
MainAxisAlignment.start, MainAxisSize mainAxisSize = MainAxisSize.max,
CrossAxisAlignment crossAxisAlignment = CrossAxisAlignment.center,
TextDirection? textDirection, VerticalDirection verticalDirection =
VerticalDirection.down, TextBaseline? textBaseline, List<Widget>
children = const <Widget>[]})

Figura 5.5 Row ed Expanded.

Procediamo con un esempio:


child: Row(
children: const <Widget>[
Expanded(
child: Text("Lorem ipsum dolor sit amet, consectetur adipisci
elit, sed do eiusmod tempor incidunt ut labore et dolore magna aliqua.
Ut enim ad minim veniam, quis nostrum exercitationem ullamco laboriosam,
nisi ut aliquid ex ea commodi consequatur", textAlign:
TextAlign.center),
),
Expanded(
child: Text("Duis aute irure reprehenderit in voluptate velit
esse cillum dolore eu fugiat nulla pariatur. Excepteur sint obcaecat
cupiditat non proident, sunt in culpa qui officia deserunt mollit anim
id est laborum.", textAlign: TextAlign.center),
),
],
),

Column
Le colonne in Flutter vengono gestite tramite la classe
Column. Il widget Column allinea verticalmente i figli.
Column( {Key? key, MainAxisAlignment mainAxisAlignment =
MainAxisAlignment.start, MainAxisSize mainAxisSize = MainAxisSize.max,
CrossAxisAlignment crossAxisAlignment = CrossAxisAlignment.center,
TextDirection? textDirection, VerticalDirection verticalDirection =
VerticalDirection.down, TextBaseline? textBaseline, List<Widget>
children = const <Widget>[]} )

Come si può vedere dal suo costruttore (parametri senza ?), i


parametri mainAxisAlignment, mainAxisSize, crossAxisAlignment e
verticalDirection non possono essere null. Nel nostro caso,
usando MaterialApp questi saranno già valorizzati.
child: Column(
children: const <Widget>[
Text("Lorem ipsum dolor sit amet, consectetur adipisci elit,
sed do eiusmod tempor incidunt ut labore et dolore magna aliqua. Ut enim
ad minim veniam, quis nostrum exercitationem ullamco laboriosam, nisi ut
aliquid ex ea commodi consequatur", textAlign: TextAlign.center),
Text("Duis aute irure reprehenderit in voluptate velit esse
cillum dolore eu fugiat nulla pariatur. Excepteur sint obcaecat
cupiditat non proident, sunt in culpa qui officia deserunt mollit anim
id est laborum.", textAlign: TextAlign.center),
],
),
Figura 5.6 Column.

Esattamente come per il widget Row, il widget Column può


essere usato con il widget Expanded.
child: Column(
children: const <Widget>[
Expanded(
child: Text("Lorem ipsum dolor sit amet, consectetur adipisci
elit, sed do eiusmod tempor incidunt ut labore et dolore magna aliqua.
Ut enim ad minim veniam, quis nostrum exercitationem ullamco laboriosam,
nisi ut aliquid ex ea commodi consequatur", textAlign:
TextAlign.center),
),
Expanded(
child: Text("Duis aute irure reprehenderit in voluptate velit
esse cillum dolore eu fugiat nulla pariatur. Excepteur sint obcaecat
cupiditat non proident, sunt in culpa qui officia deserunt mollit anim
id est laborum.", textAlign: TextAlign.center),
),
],
),
Figura 5.7 Column ed Expanded.

Container
Il widget Container crea uno spazio in cui può essere
posizionato un figlio. Possiamo immaginarlo come un
contenitore. Il container può applicare un margine al di fuori
dello spazio del container e un padding tra il bordo del container
e il figlio.
Container({Key? key, AlignmentGeometry? alignment, dgeInsetsGeometry?
padding, Color? color, Decoration? decoration, Decoration?
foregroundDecoration, double? width, double? height, BoxConstraints?
constraints, EdgeInsetsGeometry? margin, Matrix4? transform,
AlignmentGeometry? transformAlignment, Widget? child, Clip clipBehavior
= Clip.none})

L’uso della classe BoxDecoration consente di applicare uno


stile grafico, peresempio applicando uno “shape” è possibile
modificarne la forma.
body: Center(
child: Container(
margin: const EdgeInsets.all(5),
color: const Color.fromARGB(255, 0, 0, 0),
width: 200.0,
height: 200.0,
),
),

Figura 5.8 Container.

child: Container(
margin: const EdgeInsets.all(5),
width: 200.0,
height: 200.0,
decoration: const BoxDecoration(
color: Colors.black,
shape: BoxShape.circle
),
),

Figura 5.9 BoxDecoration.

Teniamo a mente un aspetto che potrebbe non essere così


scontato e Container ci offre la possibilità di apprenderlo
velocemente. Il disegno degli elementi sul layout necessita di 4
aspetti fondamentali: altezza, larghezza, coordinate x ed y.
body: Container(
height: 300,
width: 300,
decoration: BoxDecoration(border: Border.all()),
child: Container(
height: 50,
width: 50,
decoration: BoxDecoration(border: Border.all()),
),
),

Se eseguiamo questo codice, ci rendiamo immediatamente


conto che il container figlio non avrà le dimensioni attese.
Questo accade poiché non stiamo indicando a Flutter dove
posizionare le coordinate x e y per il disegno del Container
child.
Per risolvere il problema possiamo indicare l’allineamento
tramite la property alignment, piuttosto che usare il widget
Center, o Align.

Stack
Il widget Stack consente di definire uno spazio in cui i figli
vengono sovrapposti. Possiamo immaginarlo, per chi ha
dimestichezza con il web, con una posizione “absolute” in CSS.
Ovvero i figli, di default, verranno posizionati partendo dal pixel
dell’angolo in alto a sinistra del widget Stack.
Stack({Key? key, AlignmentGeometry alignment =
AlignmentDirectional.topStart, TextDirection? textDirection, StackFit
fit = StackFit.loose, Clip clipBehavior = Clip.hardEdge, List<Widget>
children = const <Widget>[]})

Facciamo un esempio.
body: Center(
child: Stack(
children: [
Container(
width: 250,
height: 250,
decoration: const BoxDecoration(
color: Colors.black,
shape: BoxShape.circle
),
), //Container
Container(
width: 200,
height: 200,
decoration: BoxDecoration(
color: Theme.of(context).canvasColor,
shape: BoxShape.circle
),
), //Container
Container(
height: 25,
width: 25,
decoration: const BoxDecoration(
color: Colors.black,
shape: BoxShape.circle
),
), //Container
], //<Widget>[]
),
),

Come si evince dalla Figura 5.10, i widget vengono


posizionati dal pixel top:0 e left:0. Il primo widget disegna un
cerchio nero, il secondo un cerchio con il colore della canvas
(sfondo dell’app) e infine l’ultimo widget disegna un piccolo
cerchio di 25 pixel posizionato esattamente all’inizio del widget
stack. In questo modo riproduciamo l’effetto in Figura 5.10. È
possibile posizionare un figlio identificando un punto preciso del
widget stack, orientandosi con le coordinate top, left, right,
bottom.
Modifichiamo l’ultimo child del codice appena mostrato, per
utilizzare la classe Positioned.
Positioned(
top: 100,
left: 100,
child: Container(
height: 25,
width: 25,
decoration: const BoxDecoration(
color: Colors.black,
shape: BoxShape.circle
),
), //Container
)
Figura 5.10 Stack.
Figura 5.11 Positioned.

Nei prossimi capitoli tratteremo la maggior parte dei widget


che Flutter offre, anche quelli descritti in questo capitolo.
verranno approfonditi entrando nel dettaglio dei parametri dei
costruttori che consentono di applicare configurazioni differenti.
Intanto, quanto finora affrontato vi consentirà di iniziare a
muovere i primi passi.
Capitolo 6

Routing

Il concetto di routing e di navigazione porta con sé uno degli


aspetti base della creazione di un’app, ovvero la possibilità di
muoversi tra le varie schermate che compongono l’applicazione.
Nel capitolo precedente abbiamo già intravisto il widget
Navigator e grazie a esso ci siamo spostati su una nuova
schermata, gestendo poi la possibilità di tornare indietro. Il
termine navigazione indica esattamente la possibilità di
“spostarsi” tra le pagine che compongono la nostra
applicazione, mentre con il termine routing generalmente viene
indicato il modo in cui questa feature è implementata. Flutter
offre più di un modo per gestire la navigazione.
NOTA
La schermata può essere chiamata anche in altri modi; tra italiano e
inglese abbondano le possibilità: pagina, vista, percorso, screen, view,
path…

Navigator
Il widget Navigator consente di muoversi tra le schermate
dell’app utilizzando un approccio “navigation history stacks”.
Navigator gestisce la navigazione approciandola come fosse
una pila. Il concetto di pila dovrebbe essere familiare ai più. Se
non vi è chiaro, potete immaginare una gestione di questo tipo:
una volta avviata l’app, la schermata home è il primo elemento
in cima alla pila; se l’utente si muoverà tra le schermate,
Navigator gestirà dinamicamente l’inserimento o la rimozione
dell’elemento in cima alla pila, mostrando sempre la schermata
che corrisponde all’elemento in cima.
Facciamo un esempio.

Listato 6.1 main.dart


import 'package:flutter/material.dart';

void main() {
runApp(const MyApp());
}

class MyApp extends StatelessWidget {


const MyApp({super.key});

@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: const MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}

class MyHomePage extends StatefulWidget {


const MyHomePage({super.key, required this.title});

final String title;

@override
State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: const <Widget>[
Text('Home page'),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: () =>
Navigator.of(context).push(MaterialPageRoute(builder: (context) => const
NewPage())),
child: const Text('New'),
),
);
}
}

class NewPage extends StatefulWidget {


const NewPage({super.key});
@override
State<NewPage> createState() => _NewPageState();
}

class _NewPageState extends State<NewPage> {

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Second Page'),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: const <Widget>[
Text('Second page'),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: () =>
Navigator.of(context).push(MaterialPageRoute(builder: (context) => const
NewPage())),
child: const Text('New'),
),
);
}
}

L’esempio qui sopra ci aiuta a comprendere meglio il concetto


di stack navigation. Se procediamo creando sempre più pagine,
vedremo che sarà necessario tornare indietro lo stesso numero
di volte che abbiamo usato il bottone New. Ciò avviene proprio
per l’inserimento dell’elemento di navigazione in cima alla pila:
con il metodo push.
Flutter autonomamente gestisce il bottone back (back button)
per tornare indietro. Nei device in cui il tasto non è già presente
nella barra di navigazione nativa (iOS), l’appBar aggiungerà il
tasto back; questo comportamento è gestito automaticamente
da Flutter.
Il back button esegue il metodo pop, l’opposto di push, offerto
dal widget Navigator. Procediamo creando un ulteriore bottone
per tornare indietro.
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
const Text('Second page'),
ElevatedButton(
style: ElevatedButton.styleFrom(textStyle: const
TextStyle(fontSize: 12)),
onPressed: () => Navigator.pop(context),
child: const Text('Back'),
),
],

),
),

Navigator usa i metodi push e pop per aggiungere e


rimuovere le rotte: questo approccio è definito come
“imperativo”.
Future<T?> push<T extends Object?>(
BuildContext context,
Route<T> route
)

void pop<T extends Object?>(


BuildContext context,
[T? result]
)

NOTA
L’uso dei generici in Dart porta con sé la convenzione della definizione
dei tipi generici, che per i Future è definita come T.

Entrambi i metodi hanno come argomento il BuildContext,


fondamentale per gestire la modifica al widget tree durante il
cambio di rotta. Inoltre, il metodo ha un secondo parametro,
chiamato route, che può essere usato come istanza della classe
MaterialPageRoute, che ci consente di applicare durante il
cambio di schermata una “transition animation”. Inoltre, push
ritorna un Future, che si concluderà quando la stessa schermata
verrà rimossa tramite pop.
La classe MaterialPageRoute è ereditata da Route, ed è
utilizzata per gestire la navigazione verso la nuova rotta.
MaterialPageRoute<T>(

{required WidgetBuilder builder,

RouteSettings? settings,

bool maintainState = true,

bool fullscreenDialog = false})

NOTA
I costrutti possono usare la chiave required per obbligare l’uso del
parametro, piuttosto che il carattere ? per esplicitarlo come opzionale o
anche definire un parametro con un valore di default tramite il carattere
=.

Come vediamo dal costrutto, è possibile gestire ulteriori


parametri per configurare dettagli relativi al routing. Nel nostro
caso possiamo fare una semplice prova modificando il
parametro fullscreenDialog a true, per vedere effettivamente
come cambia la user experience.
Per quanto l’uso di una finestra di dialogo non cambi nulla dal
punto di vista funzionale della navigazione, la user experience
invece è diversa. La finestra di dialogo, rispetto a un cambio di
vista. ha transizione differente; per esempio su iOS la finestra
arriva con una animazione dal basso verso l’alto e inoltre il tasto
back viene sostituto dal close.
In effetti, la user experience è diversa poiché da una finestra
di dialogo ci si aspetta un elemento che si sovrappone a una
navigazione, non da usare come rotta.
MaterialPageRoute(builder: (context) => const NewPage())

L’istanza di MaterialPageRoute associerà la nuova rotta a un


widget che potrà essere definito tramite un WidgetBuilder.
NOTA
Non ci sono differenze tra l’uso di Navigator.of(context) o
Navigator.pop/push(context).

Named Routes
Le rotte definite con un “nome”, consentono una navigazione
alternativa a quanto appena visto. Questa tipologia di
navigazione si rende preferibile quando si hanno un buon
numero di pagine. L’uso di questa tipologia di rotte consente
accedere a una determinata rotta tramite un nome. La
differenza, rispetto a un routing imperativo, è che in questo caso
è necessario definire una mappa che associ i nomi delle rotte ai
relativi widget.
Modifichiamo la classe MyApp per gestire la navigazione con i
named routes.
class MyApp extends StatelessWidget {
const MyApp({super.key});

@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
initialRoute: '/',
routes: {
'/': (context) => const MyHomePage(title: 'Flutter Demo Home
Page'),
'/second': (context) => const NewPage(),
},
);
}
}

Il widget MaterialApp è il punto in cui definire la mappa delle


rotte e la home. Il parametro initialRoute corrisponde alla prima
rotta da impostare: diciamo che può essere considerato il
corrispettivo del parametro home in presenza di routing di questo
tipo. Infine, il parametro routes definisce la mappa per le singole
rotte.
Rispetto all’uso del metodo push, dobbiamo usare pushNamed
per aggiungere la nuova rotta allo stack.
floatingActionButton: FloatingActionButton(
onPressed: () => Navigator.pushNamed(context, '/second'),
child: const Text('New'),
),

Per eliminare la rotta dallo stack e tornare “indietro” è


necessario procedere esattamente come visto in precedenza:
Navigator.pop(context).

È curioso e coerente il comportamento che otterreste se


usaste Navigator.pushNamed(context, '/') per tornare alla home, in
quanto il back button rimarrebbe presente nell’appBar,
nonostate l’utente si trovi nella home.
Questo comportamento è sintomatico del fatto che,
nonostante l’utente si sposti nella home con un pushNamed(context,
'/'), di fatto viene semplicemente aggiunta la rotta nello stack e
non accadrebbe con l’uso del metodo pop.
Passaggio dati
Nel caso del routing imperativo è possibile passare dei dati
alla nuova schermata semplicemente gestendo i dati come
parametri del widget associato alla schermata.
Possiamo modificare il codice di esempio introducendo
questa nuova logica.

onPressed: () {
Navigator.of(context).push(MaterialPageRoute(
builder: (context) => NewPage(data: widget.title)));
},

class NewPage extends StatefulWidget {


final String data;
const NewPage({super.key, required this.data});

Nell’esempio named router è necessario usare un approccio


diverso. Il metodo pushNamed dovrà fare uso della mappa arguments.
Navigator.pushNamed(
context,
'/second',
arguments: {'widgetTitle': widget.title},
);

Nel widget associato a /second, arguments potrà essere letto


tramite ModalRoute.of, che consente di accedere ai settings
dell’istanza di routing.
final arguments = (ModalRoute.of(context)?.settings.arguments ??
<String, String>{}) as Map;

Sarà infine possibile accedere al singolo argomento tramite la


mappa arguments:
Text(arguments['widgetTitle'])

Concludiamo il paragrafo con il solito esempio, ma questa


volta utilizzando i named routes con gli arguments.

Listato 6.2 main.dart


import 'package:flutter/material.dart';

void main() {
runApp(const MyApp());
}

class MyApp extends StatelessWidget {


const MyApp({super.key});

@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
initialRoute: '/',
routes: {
'/': (context) => const MyHomePage(title: 'Flutter Demo Home
Page'),
'/second': (context) => const NewPage(),
},
);
}
}

class MyHomePage extends StatefulWidget {


const MyHomePage({super.key, required this.title});

final String title;

@override
State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {


@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: const <Widget>[
Text('Home page'),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: () {
Navigator.pushNamed(
context,
'/second',
arguments: {'widgetTitle': widget.title},
);
},
child: const Text('New'),
),
);
}
}

class NewPage extends StatefulWidget {


const NewPage({super.key});

@override
State<NewPage> createState() => _NewPageState();
}

class _NewPageState extends State<NewPage> {


@override
Widget build(BuildContext context) {
final arguments =
(ModalRoute.of(context)?.settings.arguments ?? <String, String>
{})
as Map;

return Scaffold(
appBar: AppBar(
title: const Text('Second Page'),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(arguments['widgetTitle']),
],
),
),
);
}
}

Navigator 2.0
La navigazione, nell’accezione del Navigator 2.0, è usata in
contesti in cui l’app interagisce con deep links o ancora meglio
necessita di una navigazione maggiormente basata sugli URL.
Questa tipologia di navigazione si basa sulla classe Router di
Flutter, intorno a cui è stato costruito un package chiamato
go_router.
NOTA
Il deep link è una caratteristica che consente di interagire con l’app
tramite un collegamento esterno.

Per ottenere go_router è necessario ricorrere al package


manager di Dart, pub. Per installare go_package non dovremmo
fare altro che usare questo comando, all’interno della nostra
directory di lavoro:
# flutter pub add go_router

NOTA
Potete notare che, dopo l’installazione di go_router, Flutter aggiunge
questa dipendenze automaticamente nei file pubspec.

Figura 6.1 go_router.

Modifichiamo hello_world per cambiare il routing con go_router.


Listato 6.3 main.dart
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';

final GoRouter _router = GoRouter(


routes: <RouteBase>[
GoRoute(
path: '/',
builder: (BuildContext context, GoRouterState state) {
return const MyHomePage(title: 'Flutter GoRoute Demo');
},
routes: <RouteBase>[
GoRoute(
path: 'second',
builder: (BuildContext context, GoRouterState state) {
return const NewPage();
},
),
],
),
],
);

void main() {
runApp(const MyApp());
}

class MyApp extends StatelessWidget {


const MyApp({super.key});

@override
Widget build(BuildContext context) {
return MaterialApp.router(
theme: ThemeData(
primarySwatch: Colors.blue,
),
routerConfig: _router,
);
}
}

class MyHomePage extends StatefulWidget {


const MyHomePage({super.key, required this.title});

final String title;

@override
State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {


@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: const <Widget>[
Text('Home page'),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: () => GoRouter.of(context).go('/second'),
child: const Text('New'),
),
);
}
}

class NewPage extends StatefulWidget {


const NewPage({super.key});
@override
State<NewPage> createState() => _NewPageState();
}

class _NewPageState extends State<NewPage> {


@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Second Page'),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: const <Widget>[
Text('Second page'),
],
),
),
);
}
}

Il comportamento dell’app è esattamente lo stesso degli


approcci precedenti, con una grande differenza: GoRouter
consente di gestire le rotte come vere e proprie URL. Nel nostro
caso abbiamo questa configurazione.
final GoRouter _router = GoRouter(
routes: <RouteBase>[
GoRoute(
path: '/',
builder: (BuildContext context, GoRouterState state) {
return const MyHomePage(title: 'Flutter GoRoute Demo');
},
routes: <RouteBase>[
GoRoute(
path: 'second',
builder: (BuildContext context, GoRouterState state) {
return const NewPage();
},
),
],
),
],
);

GoRouter viene istanziato con una struttura definita tramite la


property routes. Ogni rotta viene creata tramite il costruttore
GoRoute, che ha come parametri il path e il relativo
WidgetBuilder.
Un altro passaggio fondamentale è quello di modificare,
rispetto ai precedenti esempi, il widget MaterialApp, che utilizza
il parametro routerConfig per agganciare GoRoute.
..
return MaterialApp.router(
theme: ThemeData(
primarySwatch: Colors.blue,
),
routerConfig: _router,
);
..

Il passaggio ultimo è la navigazione e, in questo contesto,


possiamo usare due sintassi che hanno lo stesso risultato:
GoRouter.of(context).go('/second'),

o
context.go('/second'),

Homepage
Possiamo fare uso delle property initialLocation quando
configuriamo l’istanza di GoRouter. La property initialLocation
consente di indicare un path di default diverso da /.
final GoRouter _router = GoRouter(
initialLocation: '/second',
routes: <RouteBase>[
GoRoute(

Location
Potremmo avere la necessità di sapere in quale rotta si trova
un determinato widget.
In questo caso possiamo fare uso dell’operatore of, offerto da
GoRouter.
Modifichiamo lo stato _NewPageState per accedere alla property
location.

class _NewPageState extends State<NewPage> {


@override
Widget build(BuildContext context) {
final router = GoRouter.of(context);
return Scaffold(
appBar: AppBar(
title: Text(router.location),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: const <Widget>[
Text('User page'),
],
),
),
);
}
}
Figura 6.2 location.

Sub-Routes
La configurazione appena usata fa uso delle sub-routes. La
definizione della rotta second, se notate bene, appartiene al
path definito con /. L’uso di una rotta child (chiamata anche
così) riproduce il comportamento di Navigator.push (mi riferisco
al back button). Per comprendere meglio il concetto di sub-
routes proviamo a definire diversamente le due rotte, creandole
allo stesso livello.
routes: <RouteBase>[
GoRoute(
path: '/',
builder: (BuildContext context, GoRouterState state) {
return const MyHomePage(title: 'Flutter GoRoute Demo');
}
),
GoRoute(
path: '/third',
builder: (BuildContext context, GoRouterState state) {
return const ThirdPage();
}
),
],

In questa configurazione possiamo notare come il back


nell’appBar non venga attivato quando ci troviamo sulla rotta
/third . Questo accade poiché solo i figli vengono inseriti nella

navigation history stacks, creando così uno storico di


navigazione.
Parametri
Come una vera e propria URL, go_router consente l’uso e il
passaggio di parametri. I dati possono essere passati come
parametri o query string. La differenza possiamo vederla in
questi due esempi:
/user/pippo

o
/user?userId="pippo"

Queste due casistiche vengono gestite diversamente nella


configurazione di go_router.

Param property
Nel primo caso, ipotizziamo una URL che mostra la
schermata di dettaglio di uno user:
/user/pippo

Pippo è l’ username dell’utente. Il primo passo è definire la


rotta.
GoRoute(
path: 'user/:userId',
builder: (BuildContext context, GoRouterState state) {
return NewPage(user: state.params['userId']!);
},
),

NOTA
L’uso dell’operatore !, dopo una variabile, ci consente di esplicitare al
compilatore che la variabile in questione è “not nullable”, in altre parole
la variabile non sarà mai null.

Proseguiamo immaginando un widget che dovrà navigare


verso questa rotta:
floatingActionButton: FloatingActionButton(
onPressed: () => GoRouter.of(context).go('/user/pippo'),
child: const Text('User'),
),
Ciò che rimane da fare è gestire il parametro e accedere alla
property pippo.
Questo può essere fatto definendo un nuovo parametro nel
costruttore.
class NewPage extends StatefulWidget {
final String user;
const NewPage({super.key, required this.user});
@override
State<NewPage> createState() => _NewPageState();
}

class _NewPageState extends State<NewPage> {

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.user),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: const <Widget>[
Text('User page'),
],
),
),
);
}
}

Come già visto in precedenza, dallo state è possibile


accedere al suo widget e, di conseguenza, alle properties
dell’istanza del widget tramite la variabile widget. Il widget Text
non mostrerà altro che lo username del nostro user: Pippo.

Figura 6.3 go_router parameter.


Query string
Riproduciamo quanto appena visto utilizzando una rotta URL
con un query string.
GoRoute(
path: 'user',
builder: (BuildContext context, GoRouterState state) {
return NewPage(user: state.queryParams['userId']!);
},
),

L’oggetto state ci consente di accedere al parametro userId. In


questo modo il parametro verrà passato al costruttore,
esattamente come nell’esempio precedente.
NOTA
Nel caso in cui volessimo condividere un oggetto tra rotte, GoRouter lo
consente tramite l’uso del parametro extra.
https://docs.page/csells/go_router/parameters#extra-parameter.

Named Router
GoRouter gestisce, così come visto nel paragrafo Navigator,
anche le named routes.
GoRoute(
name: 'user',
path: 'user/:userId',
builder: (BuildContext context, GoRouterState state) {
return NewPage(user: state.params['userId']!);
},
),

In questo modo sarà possibile navigare la rotta usando il


metodo goNamed, gestendo i parametri come una mappa.
floatingActionButton: FloatingActionButton(
onPressed: () => GoRouter.of(context).goNamed('user', params:
{'userId': 'pippo'}),
child: const Text('User'),
),

Questo approccio darà il medesimo risultato, ma sarà più


ordinato nel caso in cui si debbano navigare rotte più complesse
che prevedono la gestione contemporanea di più parametri.

Redirect
GoRouter ci consente di gestire il redirect: questa particolare
feature è attivabile tramite una property dell’istanza GoRouter.
Immaginiamo lo scenario in cui sia possibile accedere a una
specifica schermata solo se prima si è effettuato un login.
Ancora più semplicemente, possiamo creare un esempio in cui,
per accedere a una rotta, è necessario navigarne prima un’altra.
Modifichiamo la rotta /third, gestendo la property redirect.
GoRoute(
path: '/third',
builder: (BuildContext context, GoRouterState state) {
return const ThirdPage();
},
redirect: (context, state) {
debugPrint('isLogged: ${loginInfo.isLogged}');
if (loginInfo.isLogged){
return "/third";
}
return "/";
},
),

Come si evince dal codice, la logica è quella di verificare che


la condizione di accesso o meno alla rotta sia soddisfatta. In
entrambi i casi viene ritornata una stringa che identifica e
indirizza l’utente alla rotta indicata. Nel nostro caso verifichiamo
semplicemente che logInfo.isLogged sia true.
logInfo è l’istanza di una classe, che viene definita
“globalmente”.
class LoginInfo {
bool isLogged = false;
}
final loginInfo = LoginInfo();

final GoRouter _router = GoRouter(


initialLocation: '/',
routes: <RouteBase>[

Nonostante ci siano pratiche più consone per condividere dati


tra widget, in questo modo riusciamo comunque a raggiungere
lo scopo prefissato.
La modifica della property isLogged può avvenire, per esempio,
nella classe relativa a un’altra rotta.
class _NewPageState extends State<NewPage> {
@override
Widget build(BuildContext context) {
loginInfo.isLogged=true;

Lo stato _NewPageState che si cela dietro la rotta /second


cambierà il valore della property isLogged, di conseguenza sarà
possibile accedere alla rotta third.

Deep linking
Il deep linking è una feature che consente di accedere a una
rotta interna direttamente tramite un URL. Per quanto sul web
sia piuttosto semplice, per i dispositivi mobili è necessaria una
configurazione specifica per Android ed iOS.

Android
Per android è necessario modificare il file
AndroidManifest.xml in android/app/src.
Aggiungere questa porzione di configurazione all’interno
dell’activity principale.
<meta-data android:name="flutter_deeplinking_enabled"
android:value="true" />
<intent-filter android:autoVerify="true">
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="http" android:host="exampledeeplinking.com" />
<data android:scheme="https" android:host="exampledeeplinking.com" />
</intent-filter>

È possibile effettuare un test tramite il tool adb.


adb shell am start -a android.intent.action.VIEW \
-c android.intent.category.BROWSABLE \
-d "http://exampledeeplinking.com/second" \
com.example.test

iOS
Per iOS il file da modificare è Info.plist, contenuto in ios/Runner.
<key>FlutterDeepLinkingEnabled</key>
<true/>
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleTypeRole</key>
<string>Editor</string>
<key>CFBundleURLName</key>
<string>exampledeeplinking.com</string>
<key>CFBundleURLSchemes</key>
<array>
<string>customscheme</string>
</array>
</dict>
</array>

È possibile effettuare un test tramite il tool xcrun.


xcrun simctl openurl booted customscheme://exampledeeplinking.com/second

In entrambi gli esempi il comportamento sarà coerente,


ovvero sarà possibile accedere tramite una risorsa esterna a
una risorsa interna, in questo caso la rotta /second.
Fate attenzione a specificare la tipologia di schema da
utilizzare per il deep link. In iOS è customscheme, lo schema definito
tramite la key CFBundleURLSchemes. Per Android possiamo utilizzare
uno custom o, come introdotto nell’esempio appena presentato,
utilizzare gli schemi http/https. In questo caso è importante
definire la property android:autoVerify, poiché il sistema operativo,
alla richiesta di accedere alla risorsa http/https definita nel file
AndroidManifest dell’applicazione, dovrà controllare se la risorsa
è verificata. Se la risorsa richiesta è verificata verrà rimandata
all’app, diversamente al browser.
NOTA
Un esempio di deep link è il classico comportamento che si vede
quando da mobile proviamo ad aprire un URL di YouTube e il sistema
ci chiede se navigare l’url dall’app o dal browser.
Capitolo 7

Widget Material Design

Flutter offre un catalogo di widget veramente cospicuo.


Abbiamo già affrontato il tema widget, spiegando che cosa sia e
quale sia il suo lifecycle. Vediamo adesso di catalogare i widget
più popolari, quanto meno quelli che sono maggiormente
utilizzati.

Material
Il design Material è alla base di una serie di widget figli del
widget MaterialApp. Il suo stile è supportato nativamente da
Android, ma è ottimizzato anche per i sistemi iOS. Possiamo
sviluppare la nostra applicazione usando il Material design
anche su iOS senza preoccuparci di dover necessariamente
usare lo stile Apple (Cupertino).

MaterialApp
Tutto parte dal widget MaterialApp, che abbiamo finora
incontrato numerose volte.
const MaterialApp(

{Key? key,
GlobalKey<NavigatorState>? navigatorKey,
GlobalKey<ScaffoldMessengerState>? scaffoldMessengerKey,
Widget? home,
Map<String, WidgetBuilder> routes = const <String, WidgetBuilder>{},
String? initialRoute,
RouteFactory? onGenerateRoute,
InitialRouteListFactory? onGenerateInitialRoutes,
RouteFactory? onUnknownRoute,
List<NavigatorObserver> navigatorObservers = const <NavigatorObserver>
[],
TransitionBuilder? builder,
String title = ‘’,
GenerateAppTitle? onGenerateTitle,
Color? color,
ThemeData? theme,
ThemeData? darkTheme,
ThemeData? highContrastTheme,
ThemeData? highContrastDarkTheme,
ThemeMode? themeMode = ThemeMode.system,
Duration themeAnimationDuration = kThemeAnimationDuration,
Curve themeAnimationCurve = Curves.linear,
Locale? locale,
Iterable<LocalizationsDelegate>? localizationsDelegates,
LocaleListResolutionCallbackLocaleResolutionCallback?
localeResolutionCallback,
Iterable<Locale> supportedLocales = const <Locale>[Locale(‘en’, ‘US’)],
bool debugShowMaterialGrid = false,
bool showPerformanceOverlay = false,
bool checkerboardRasterCacheImages = false,
bool checkerboardOffscreenLayers = false,
bool showSemanticsDebugger = false,
bool debugShowCheckedModeBanner = true,
Map<, Intent>? shortcuts,
Map<Type, Action<Intent>>? actions,
String? restorationScopeId,
ScrollBehavior? scrollBehavior,
bool useInheritedMediaQuery = false}

Come possiamo vedere dal costruttore, MaterialApp ha


parecchi parametri, proprio perché è la base su cui costruire
un’applicazione di tipo material.
L’esempio base utilizzato più volte è questo:
MaterialApp(
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: const MyHomePage(title:'Home'),
);

I parametri che potremmo utilizzare sono i seguenti.


key: parametro comune per tantissime tipologie di widget,
necessarie in alcune casistiche che tratteremo più avanti.
navigatorKey: definire questa key consente di accedere alla
navigazione utilizzando navigatorKey.currentContext piuttosto
che Navigator.of(context). Nonostante l’uso di una
navigatorKey possa semplificare lo sviluppo, consiglio di
limitare il suo utilizzo con un design progettuale più
accurato.
scaffoldMessengerKey: key utilizzata per accedere all’istanza di

ScaffoldMessenger senza l’uso di un context.


ScaffoldMessenger è un widget che consente di gestire le
snack bar su più Scaffold (le snack bar sono delle notifiche
mostrate nella parte inferiore della schermata).
home: widget root.

routes: definizione delle rotte di navigazione.


initialRoute: nome che identifica la prima rotta di
navigazione.
onGenerateRoute: è una callback che viene utilizzata per
generare una rotta qualora la rotta cercata non esista.
onGenerateInitialRoutes: rotta iniziale rispetto alle rotte

“generate”.
onUnknownRoute: usato per gestire gli errori di routing. Per
esempio, se una rotta non è presente possiamo dirottare
l’utente su una pagina dedicata.
navigatorObservers: istanza di RouteObserver che consente di

implementare tramite mixin una serie di metodi che


vengono richiamati durante la fase di navigazione del
nostro widget.
builder: la funzione di builder può essere utilizzata per

sovrascrivere i comportamenti di default della navigazione,


implementando un comportamento custom.
title: stringa che viene mostrata in Android nella schermata
delle “applicazioni recenti”.
onGenerateTitle: callback che può essere usata per generare

il title.
color: colore primario mostrato dal sistema operativo

quando interagisce con l’app. Per esempio su Android è


riscontrabile nella schermata Applicazioni recenti.
theme: tema primario dell’applicazione. Usualmente è definito

tramite la classe ThemeData.


darkTheme: tema associato al dark mode.

NOTA
Dark mode è un’impostazione offerta dal sistema operativo che
consente di ottenere dalle applicazioni (là dove possibile) delle
schermate più scure per migliorare la lettura notturna e un
conseguente risparmio energetico.

highContrastTheme e highContrastDarkTheme: per i sistemi operativi


che lo consentono, può essere implementato uno specifico
tema per l’incremento del contrasto.
themeMode: property usata per specificare la tipologia di tema:

light o dark.
themeAnimationDuration: durata dell’animazione per il cambio di
tema.
themeAnimationCurve: tipologia di Curve di animazione. Flutter
offre diverse animazioni che cambiano la velocità durante la
loro esecuzione e vengono definite Curve.
locale: identificativo della lingua usata dall’utente e viene

catturato dal sistema operativo.


localizationsDelegates: consente di definire dei delegati per la
“traduzione” dei widget.
localeListResolutionCallback: callback per gestire la
localizzazione tramite una lista di localizzazioni supportate.
localeResolutionCallback: callback per gestire una singola

localizzazione. Da preferire l’uso di


localeListResolutionCallback.
supportedLocales: lista di localizzazioni supportate.

debugShowMaterialGrid: viene sovrapposto al layout un layer


che mostra una griglia pixel per pixel.
showPerformanceOverlay: nell’app vengono mostrate

informazioni sulle performance.


checkerboardRasterCacheImages: consente di visualizzare una

griglia sulle immagini che vengono prelevate dalla cache.


checkerboardOffscreenLayers: consente di visualizzare una

griglia su layer sovrapposti.


showSemanticsDebugger: vengono evidenziati componenti con

accessibilità.
debugShowCheckedModeBanner: viene mostrato un banner se l’app
è in debug mode.
shortcuts: mappa per le scorciatoie da tastiera che è

possibile usare in app.


actions: consente di definire delle azioni che l’app può

compiere, per esempio interagire con un’altra app.


restorationScopeId: consente di gestire la cronologia della

navigazione durante uno state restoration.


scrollBehavior: consente di personalizzare la fase di scroll

per i widget “scrollabili”.


useInheritedMediaQuery: definisce se utilizzare una MediaQuery
ereditata.

NOTA
I delegati sono elementi di design del software. Immaginiamo un
delegato come un oggetto che viene in aiuto di un altro oggetto per
svolgere un determinato compito.

NOTA
La classe MediaQuery consente di catturare le informazioni relative al
layout e allo screen, tra le quali il sizing del device, l’orientamento,
pixel ratio e molte altre. Il suo uso si basa sul metodo of:
MediaQuery.of(context).

NOTA
La state restoration è un processo che in Flutter viene gestito di
default. Nella fattispecie può capitare che un’app, che viene
posizionata in background, dopo un lasso di tempo definito dal sistema
operativo venga di fatto uccisa e venga liberata la memoria allocata
dall’app. Flutter offre degli strumenti per personalizzare questo
comportamento al suo restore.

Abbiamo introdotto nel dettaglio tutti i parametri relativi al


widget MaterialApp per mostrare sommariamente quante
possibilità di personalizzazione ci siano. Nei prossimi widget ci
soffermeremo solo sui parametri più rilevanti, tralasciando
configurazioni legate a concetti più avanzati rispetto a quelli
necessari a raggiungere l’obiettivo di questo libro.

Scaffold
Il widget Scaffold è il widget che fornisce la struttura di base
per il Material design. La sua traduzione italiana aiuta
sicuramente a esprimere in maniera più efficace il concetto:
impalcatura. Il widget in questione implementa una serie di
widget che si innestano allo Scaffold tramite specifiche
properties.
Scaffold({Key? key, PreferredSizeWidget? appBar, Widget? body, Widget?
floatingActionButton, FloatingActionButtonLocation?
floatingActionButtonLocation, FloatingActionButtonAnimator?
floatingActionButtonAnimator, Lis<>? persistentFooterButtons,
persistentFooterAlignment = AlignmentDirectional.centerEnd, ? drawer, ?
onDrawerChanged, ? endDrawer, ? onEndDrawerChanged, ?
bottomNavigationBar, ? bottomSheet, ? backgroundColor, ?
resizeToAvoidBottomInset, primary = true, drawerDragStartBehavior =
DragStartBehavior.start, extendBody = false, extendBodyBehindAppBar =
false, ? drawerScrimColor, ? drawerEdgeDragWidth,
drawerEnableOpenDragGesture = true, endDrawerEnableOpenDragGesture =
true, ? restorationId})

Widget build(BuildContext context) {


return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(

),
floatingActionButton: FloatingActionButton(
onPressed: () {

},
child: const Text('New'),
),
);
}

Alcune properties del costruttore sono effettivamente widget


che si legano allo Scaffold offrendo funzionalità legate alla user
experience. Approfondiamone qualcuna.

appbar
Finora, abbiamo visto parecchie volte il widget appbar. È
certamente il primo widget di un’app di Material design che
compone il nostro widget tree. L’appbar, come suggerisce il
nome, è una barra, posizionata in alto sullo schermo con
un’altezza fissa, che consente all’utente di interagire con
pulsanti, quindi con altri widget.
AppBar({Key? key, Widget? leading, bool automaticallyImplyLeading =
true, Widget? title, List<Widget>? actions, Widget? flexibleSpace,
PreferredSizeWidget? bottom, double? elevation, double?
scrolledUnderElevation, ScrollNotificationPredicate
notificationPredicate = defaultScrollNotificationPredicate, Color?
shadowColor, Color? surfaceTintColor, ShapeBorder? shape, Color?
backgroundColor, Color? foregroundColor, @Deprecated('This property is
no longer used, please use systemOverlayStyle instead. ' 'This feature
was deprecated after v2.4.0-0.0.pre.') Brightness? brightness,
IconThemeData? iconTheme, IconThemeData? actionsIconTheme,
@Deprecated('This property is no longer used, please use
toolbarTextStyle and titleTextStyle instead. ' 'This feature was
deprecated after v2.4.0-0.0.pre.') TextTheme? textTheme, bool primary =
true, bool? centerTitle, bool excludeHeaderSemantics = false, double?
titleSpacing, double toolbarOpacity = 1.0, double bottomOpacity = 1.0,
double? toolbarHeight, double? leadingWidth, @Deprecated('This property
is obsolete and is false by default. ' 'This feature was deprecated
after v2.4.0-0.0.pre.') bool? backwardsCompatibility, TextStyle?
toolbarTextStyle, TextStyle? titleTextStyle, SystemUiOverlayStyle?
systemOverlayStyle})

Il primo parametro che abbiamo trovato negli esempi già


presentati è senz’altro title. La key title è usualmente destinata
a un widget di tipo Text. Oltre all’uso della property title, è
possibile modificare la forma di default dell’appbar tramite la
property shape oppure aggiungere dei bottoni con delle action.
Proviamo a creare un esempio che racchiude queste properties.
Scaffold(
appBar: AppBar(
title: const Text('Example'),
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(
bottom: Radius.circular(25),
),
),
actions: <Widget>[
TextButton(
style: style,
onPressed: () => debugPrint('logout!'),
child: const Text('Logout'),
),
IconButton(
icon: const Icon(Icons.add_alert),
onPressed: () => debugPrint('alert!'),
),
],
),
In questo esempio, troviamo la classe
RoundedRectangleBorder, che consente di applicare un bordo
rettangolare con angoli arrotondati. A sua volta viene settata la
property borderRadius, tramite la classe BorderRadius. Il
metodo vertical consente di applicare un arrotondamento
verticale solo al bottom del rettangolo.
La property actions consente di definire una lista di widget che
apparterranno ai widget mostrati all’interno dell’appbar e che di
conseguenza definiscono delle azioni. Nel nostro esempio
troviamo TextButton e IconButton; entrambi consentono di
gestire un evento onPressed.
La differenza tra TextButton e IconButton è che il primo è
testuale, mentre il secondo è un’icona. IconButton è un widget
che consente l’utilizzo delle icons material. La lista delle icone è
reperibile a questo indirizzo:
https://api.flutter.dev/flutter/material/Icons-class.html.

body
La property body di Scaffold crea la struttura per ospitare il
contenuto della nostra app. Il body è appena posizionato al di
sotto dell’appBar. Il body viene ridimensionato all’apertura della
keyboard. È possibile non applicare nessun resize se
impostiamo la property resizeToAvoidBottomInset di Scaffold a false.
body: const Center(child: Text('Hello World')),

floatingActionButton
floatingActionButton è un bottone con un bordo circolare,
sovrapposto al body poiché configurato come “fluttuante”. Float
significa esattamente questo; possiamo immaginarlo come un
elemento che, nonostante il body sia scrollabile, rimarrà fisso
nella sua posizione originale.
floatingActionButton: FloatingActionButton(
onPressed: () => debugPrint('pressed!'),
child: const Icon(Icons.add),
),

bottomNavigationBar
bottomNavigationBar è un widget ancorato al bottom dell’app
per consentire una navigazione veloce tra pochi elementi. La
barra di navigazione può interagire con il tap dell’utente
sull’icona, tramite la property onTap.
bottomNavigationBar: BottomNavigationBar(
items: const <BottomNavigationBarItem>[
BottomNavigationBarItem(
icon: Icon(Icons.home),
label: 'Home',
),
BottomNavigationBarItem(
icon: Icon(Icons.home),
label: 'Contact',
),
],
currentIndex: _selectedItem,
selectedItemColor: Colors.blue,
onTap: (int index) {setState(() => _selectedItem = index);}
),

NOTA
Con il termine tap, sui device mobili, si intende quello che nei
computer tramite mouse viene definito clic.

drawer
Il drawer è un widget posto al lato del body dello Scaffold e
compare come un menu laterale che si apre all’interazione con
l’utente. La tipologia di interazione a cui si fa riferimento è quella
tramite button o tramite gesture.
NOTA
Il termine gesture è usato per indicare una interazione che l’utente
crea con la sua app tramite l’uso di uno o più dita. Approfondiremo il
concetto in Flutter in un capitolo dedicato.

Quando viene usato drawer, in automatico viene creata


l’icona hamburger e in automatico viene pushato nello stack di
navigazione il drawer. Per questa ragione, per chiudere il widget
Drawer si usa Navigator.
drawer: Drawer(
child: IconButton(
icon: const Icon(Icons.close),
onPressed: () {
setState(() {
Navigator.pop(context);
});
},
),
),

Procediamo riassumendo quanto visto generando una nuova


app, abbandonando così di fatto la nostra hello_world.
$ flutter create scaffold_example

Listato 7.1 main.dart


import 'package:flutter/material.dart';

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

class MyApp extends StatelessWidget {


const MyApp({super.key});

@override
Widget build(BuildContext context) {
return const MaterialApp(
title: 'Scaffold example',
debugShowCheckedModeBanner: false,
home: MyStatefulWidget(),
);
}
}

class MyStatefulWidget extends StatefulWidget {


const MyStatefulWidget({super.key});

@override
State<MyStatefulWidget> createState() => _MyStatefulWidgetState();
}

class _MyStatefulWidgetState extends State<MyStatefulWidget> {


int _selectedItem = 0;

@override
Widget build(BuildContext context) {
final ButtonStyle style = TextButton.styleFrom(
foregroundColor: Theme.of(context).colorScheme.onPrimary,
);
return Scaffold(
appBar: AppBar(
title: const Text('Example'),
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(
bottom: Radius.circular(25),
),
),
actions: <Widget>[
TextButton(
style: style,
onPressed: () => debugPrint('logout!'),
child: const Text('Logout'),
),
IconButton(
icon: const Icon(Icons.add_alert),
onPressed: () => debugPrint('alert!'),
),
],
),
body: const Center(child: Text('Hello World')),
floatingActionButton: FloatingActionButton(
onPressed: () => debugPrint('pressed!'),
child: const Icon(Icons.add),
),
drawer: Drawer(
child: IconButton(
icon: const Icon(Icons.close),
onPressed: () {
setState(() {
Navigator.pop(context);
});
},
)),
bottomNavigationBar: BottomNavigationBar(
items: const <BottomNavigationBarItem>[
BottomNavigationBarItem(
icon: Icon(Icons.home),
label: 'Home',
),
BottomNavigationBarItem(
icon: Icon(Icons.home),
label: 'Contact',
),
],
currentIndex: _selectedItem,
selectedItemColor: Colors.blue,
onTap: (int index) {
setState(() => _selectedItem = index);
}),
);
}
}
Figura 7.1 scaffold_example (1); scaffold_example (2); scaffold_example
(3).

SliverAppBar
Il widget SliverAppBar è un’evoluzione dell’appBar. Offre
diverse features in più, ma possiamo principalmente
immaginarla come un’appbar con la possibilità di fluttuare,
contraendosi o espandendosi durante la fase di scroll.
SliverAppBar è un’implementazione piuttosto differente di
un’appBar che, come avevamo detto, ha un’altezza fissa.
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Example'),
),
body: CustomScrollView(
slivers: <Widget>[
const SliverAppBar(
expandedHeight: 150,
flexibleSpace: FlexibleSpaceBar(
title: Text('Bar'),
),
),
SliverList(
delegate: SliverChildBuilderDelegate(
(_, int index) {
return Container(
height: 100,
margin: const EdgeInsets.all(10),
padding: const EdgeInsets.all(3.0),
decoration: const BoxDecoration(
color: Color.fromRGBO(200, 200, 200, 0.2),
border: Border(
top: BorderSide(color: Colors.grey),
bottom: BorderSide(color: Colors.grey),
left: BorderSide(color: Colors.grey),
right: BorderSide(color: Colors.grey),
),
borderRadius: BorderRadius.all(
Radius.circular(5),
),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Center(
child:
Text('$index', style: const
TextStyle(fontWeight: FontWeight.bold)),
)
],
)
);
},
childCount: 20,
),
),
],
),
);
}
Figura 7.2 SliverAppBar.

L’implementazione di SliverAppBar è strettamente legata


al widget CustomScrollView, che tratteremo in dettaglio nel
capitolo Scrollable Widget. Il widget SliverAppBar può
espandersi fino al limite espresso dalla key
expandedHeight, ed al suo interno può definire un widget
child, che nel nostro esempio è di tipo Text.
TabBar
Il widget TabBar è un componente piuttosto comune nelle
applicazioni mobili. Le tab bar offrono un sistema di
navigazione immediato verso le specifiche tab, un
comportamento piuttosto comune quando all’interno di
un’unica schermata si vuole mostrare più contenuti
separandoli logicamente.
In Flutter, l’uso delle tab bar si porta dietro tre componenti
distinti: TabBar, TabBarView e TabController e in genere la
TabBar viene innestata nel bottom dell’appBar,
NOTA
Valutate l’ipotesi di usare BottomNavigationBar per ancorare la
tab bar nella parte inferiore del layout rispetto al contrario.

Listato 7.2 main.dart


import 'package:flutter/material.dart';

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

class MyApp extends StatelessWidget {


const MyApp({super.key});

@override
Widget build(BuildContext context) {
return const MaterialApp(
title: 'Scaffold example',
debugShowCheckedModeBanner: false,
home: MyStatefulWidget(),
);
}
}

class MyStatefulWidget extends StatefulWidget {


const MyStatefulWidget({super.key});

@override
State<MyStatefulWidget> createState() => _MyStatefulWidgetState();
}

class _MyStatefulWidgetState extends State<MyStatefulWidget> with


TickerProviderStateMixin {
late TabController _tabController;
@override
void initState() {
super.initState();
_tabController = TabController(vsync: this, length: 2);
}

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Example'),
bottom: TabBar(
controller: _tabController,
tabs: const <Widget>[
Tab(
icon: Icon(Icons.home),
),
Tab(
icon: Icon(Icons.contact_page),
)
],
),
),
body: TabBarView(
controller: _tabController,
children: const <Widget>[
Center(
child: Text("Home page"),
),
Center(
child: Text("Contact page"),
),
],
),
);
}
}

In questo esempio in cui mettiamo in campo una TabBar,


facciamo uso per la prima volta di un mixin (in un sorgente
Flutter). Usiamo la classe TickerProviderStateMixin per
estendere lo state del widget MyStatefulWidget. È
necessario usare la classe TickerProviderStateMixin poiché
è una dipendenza della classe TabController. Andiamo per
ordine.
L’uso di una tab bar consente di visualizzare
orizzontalmente una lista di elementi che gestiranno una
navigazione all’interno della stessa schermata, Ogni
elemento della TabBar avrà un widget corrispettivo che
verrà visualizzato nella TabBarView. La TabBarView
iteragisce conTabBar grazie a un controller, chiamato
tabController.
appBar: AppBar(
title: const Text('Example'),
bottom: TabBar(
controller: _tabController,
tabs: const <Widget>[
Tab(
icon: Icon(Icons.home),
),
Tab(
icon: Icon(Icons.contact_page),
)
],
),
),

Come si intuisce dal codice, tramite il widget TabBar con


la property tabs possiamo creare un elenco orizzontale di
elementi, in questo caso icone. Un altro aspetto importante
è definire un controller.

Figura 7.3 TabBar.

Il passo successivo è definire TabBarView.


body: TabBarView(
controller: _tabController,
children: const <Widget>[
Center(
child: Text("Home page"),
),
Center(
child: Text("Contact page"),
),
],
),

TabBarView, in egual modo alla classe TabBar, utilizzerà


lo stesso controller. La TabBarView è composta da una lista
di widget che corrispondono allo stesso ordine delle tab
definite nella TabBar. Per intenderci, al tap dell’icona
Icons.home farà in modo che venga mostrato il primo figlio
della TabBarView, ovvero il widget Text(“Home page”).
L’ultimo step è la definizione del controller. All’interno
dello state del widget che ospita la TabBar definiamo il
controller:
class _MyStatefulWidgetState extends State<MyStatefulWidget> with
TickerProviderStateMixin {
late TabController _tabController;

È necessario istanziare la classe TabController in


initState poiché è il punto corretto per farlo, per i motivi
chiariti nel lifecycle del widget (build verrà chiamato più
volte).
@override
void initState() {
_tabController = TabController(vsync: this, length: 2);
super.initState();
}

Tabcontroller è inizializzato tramite due parametri, il


numero degli elementi che compongono la tab bar (lenght)
e vsync, ed è preposto al coordinamento tra la TabBar e la
TabBarView.
Vsync è un ticker ed è di tipo TickerProvider. Questo
argomento è strettamente legato alle animazioni di Flutter e
a come Flutter le gestisce. Approfondiremo l’argomento in
una sezione dedicata alle animazioni, ma è bene dare un
breve contesto. La funzione TabController necessita di un
ticker; in questo caso è possibile usarlo tramite la keyword
this, ma è possibile farlo grazie all’utilizzo della classe mixin

TickerProviderStateMixin.
L’uso del ticker vsync consente di gestire con grande
efficienza il rendering di un nuovo fotogramma,
intercettando così la modifica del layout introdotto dalla
gestione dei tab.
NOTA
Pensiamo al ticker come a uno speciale timer che viene invocato
periodicamente per ogni nuovo frame renderizzato. Affronteremo
il tema nel dettaglio successivamente.

Figura 7.4 TabBar (2).


TabPageSelector
La TabBar offre anche un ulteriore widget che consente
di implementare un indicatore circolare. Il suo utilizzo
permette di mostrare all’utente la tab selezionata.
Per utilizzarla nell’esempio appena creato, è necessario
modificare il body dello Scaffold. Questo è necessario
poiché il body ha un solo child e nel nostro caso è
necessario creare un nuovo widget per il TabBarController.
Se provassimo semplicemente a utilizzare, per esempio, un
widget Column e, tramite la property children, sia il widget
TabBarView che il TabBarSelector, otterremmo un errore in
fase di avvio in quanto il widget Column non ha un’altezza
delimitata e il tab TabBarView ha lo stesso comportamento.
Ciò rende impossibile il calcolo dell’altezza del figlio della
colonna. L’uso di expanded risolve il problema, in quanto i
figli di Column si espanderanno fino all’altezza possibile.
NOTA
“Horizontal viewport was given unbounded height.” Di default
alcuni widget occupano tutto lo spazio disponibile e, quando
vengono utilizzati con genitori che non impongono vincoli di
grandezza, Flutter potrebbe avere difficoltà nel rendering dei
suddetti widget.
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Example'),
bottom: TabBar(
controller: _tabController,
tabs: const <Widget>[
Tab(
icon: Icon(Icons.home),
),
Tab(
icon: Icon(Icons.contact_page),
)
],
),
),
body: Column(
children: [
Expanded(
child:TabBarView(
controller: _tabController,
children: const <Widget>[
Center(
child: Text("Home page"),
),
Center(
child: Text("Contact page"),
),
],
),
),
Padding(
padding: EdgeInsets.all(20), //apply padding to all four
sides
child: TabPageSelector(
controller: _tabController,
)
),
],
)
);
}
Figura 7.5 TabPageSelector (2).

Dialog
Le dialog sono dei pop up che notificano un evento a un
utente. In genere sono alert, ma in alcuni casi possono
anche consentire una scelta.
AlertDialog
Un classico esempio che realizzeremo con il widget
AlertDialog è un alert che vedremo comparire quando
proviamo a cancellare qualcosa di importante.
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Example'),
),
body: Center(
child: IconButton(
icon: const Icon(Icons.delete_forever),
iconSize: 52,
onPressed: () => showDialog<String>(
context: context,
builder: (BuildContext context) => AlertDialog(
title: const Text('Alert'),
content: const Text('Are you sure?'),
actions: <Widget>[
TextButton(
onPressed: () => Navigator.pop(context, 'No'),
child: const Text('No'),
),
TextButton(
onPressed: () => Navigator.pop(context, 'Yes'),
child: const Text('Yes'),
),
],
),
),
),
),
);
}
Figura 7.6 IconButton per aprire la dialog (2).

La finestra di dialogo viene aperta istanziando la classe


showDialog e i due parametri utilizzati dal costruttore
consentono di renderizzare la finestra in questione. Il
BuildContext viene usato per chiudere la finestra tramite
Navigator, che a sua volta consente di trasportare il dato
corrispondente alla scelta dell’utente. La funzione di build
corrisponde al widget AlertDialog, che costituisce il
contenuto della finestra di dialogo. I parametri actions
consentono di gestire la “scelta” a cui facevamo riferimento.

Figura 7.7 AlertDialog.

SimpleDialog
Il widget SimpleDialog è molto simile ad AlertDialog.
Rispetto a quest’ultimo, può essere pensato per un utilizzo
più generico e meno immediato. L’esempio che
presenteremo riproduce la stessa logica del precedente
sorgente.

Listato 7.3 main.dart


import 'package:flutter/material.dart';

void main() {
runApp(const MyApp());
}

class MyApp extends StatelessWidget {


const MyApp({Key? key}) : super(key: key);

@override
Widget build(BuildContext context) {
return MaterialApp(
// Hide the debug banner
debugShowCheckedModeBanner: false,
theme: ThemeData.light(),
home: const HomePage(),
);
}
}

class HomePage extends StatefulWidget {

const HomePage({super.key});

@override
State<StatefulWidget> createState() {
return _HomePageState();
}
}

class _HomePageState extends State<HomePage> {

String? result;

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Example'),
),
body: Center(
child: Column(
children: [
IconButton(
icon: const Icon(Icons.delete_forever),
iconSize: 52,
onPressed: () => showMyAlertDialog(context),
),
Text(result==null ? '':'Result $result')
],
)
)
);
}

showMyAlertDialog(BuildContext context) {
return showDialog(
context: context,
builder: (_) {
return SimpleDialog(
title: const Text('Are you sure?'),
children: [
SimpleDialogOption(
child: const Text('Yes'),
onPressed: () {
setState(() {
result = 'Option 1';
});
Navigator.of(context).pop();
},
),
SimpleDialogOption(
child: const Text('No'),
onPressed: () {
setState(() {
result = 'Option 2';
});
Navigator.of(context).pop();
},
)
],
);
},
);
}
}
Figura 7.8 SimpleDialog.

Figura 7.9 Result SimpleDialog.


Rispetto all’esempio precedente, abbiamo aggiunto altra
carne al fuoco. In questo esempio è possibile vedere come
il risultato della scelta effettuata all’interno del widget
SimpleDialog venga renderizzato nel widget HomePage.
Questo è possibile perchè, grazie al lexical scope di Dart, la
property result può essere settata all’interno delle due
SimpleDialogOption. Infine, possiamo utilizzare l’operatore
ternario per cambiare l’output di Text.
NOTA
L’operatore ternario consente di sintetizzare una struttura if-else

in una unica istruzione, schematizzata in (condizione) ? true :

false.

Avremmo potuto gestire il cambiamento della property


result in maniera più elegante, aiutandoci con una
operazione asincrona; lo vedremo in un prossimo capitolo.

Panel
I panel sono elementi meno rigidi rispetto ai dialog. Di
norma vengono utilizzati per generare un’interazione con
l’utente. Una scelta o una gesture che genera
un’interazione sono elementi di UI molto comuni.
Procediamo con alcuni esempi.

BottomSheet
Il widget bottomSheet è un panel che viene mostrato
nella parte inferiore dello schermo. Spesso si usa per
generare un’interazione con l’utente, per esempio un social
bottom.
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Example'),
),
body: Builder(
builder: (context) =>
Center(
child: ElevatedButton(
child: const Text('showBottomSheet'),
onPressed: () {
Scaffold.of(context).showBottomSheet<void>(
(BuildContext context) {
return Container(
height: 150,
color: Colors.blue,
child:Column(
children: <Widget>[
TextButton(
style: TextButton.styleFrom(fixedSize:
const Size.fromHeight(50)),
child: Row(children: const [
Icon(Icons.share, color: Colors.white,),
SizedBox(width: 10),
Text('Share', style: TextStyle(color:
Colors.white),),
]),
onPressed: ()=>{},
),
TextButton(
style: TextButton.styleFrom(fixedSize:
const Size.fromHeight(50)),
child: Row(children: const [
Icon(Icons.logout, color:
Colors.white,),
SizedBox(width: 10),
Text('Logout', style: TextStyle(color:
Colors.white),),
]),
onPressed: ()=>{},
),
TextButton(
style: TextButton.styleFrom(fixedSize:
const Size.fromHeight(50)),
child: Row(children: const [
Icon(Icons.logout, color:
Colors.white,),
SizedBox(width: 10),
Text('Close showBottomSheet', style:
TextStyle(color: Colors.white),),
]),
onPressed: ()=>{Navigator.pop(context)},
),
],
)
);
},
);
},
),
)
)

In questo esempio facciamo uso di un bottone per aprire


showBottomSheet e questo viene fatto istanziando il
costruttore tramite: Scaffold.of(context).showBottomSheet<void>
(Widget Function(BuildContext) builder). La funzione builder
ritorna il widget che sarà il contenuto di showBottomSheet.
Il widget in questione può essere chiuso tramite gesture o
tramite Navigator. L’uso del costrutto Scaffold.of(context) è
necessario poiché showBottomSheet è un widget che si
innesta nello Scaffold.
NOTA
In Figura 7.11 possiamo notare come, avvolgendo Scaffold con il
widget SafeArea, arginiamo la problematica relativa al confine del
bottom in Figura 7.10, altrimenti governabile in altri modi.
Figura 7.10 showBottomSheet.
Figura 7.11 showBottomSheet con SafeArea.

Un’alternativa all’uso del widget showBottomSheet


tramite Scaffold è utilizzare il widget
showModalBottomSheet. La differenza sta nel fatto che
l’uso di showModalBottomSheet focalizza l’attenzione e
inibisce le operazioni al di fuori del bottomsheet, mentre nel
caso di showBottomSheet il body dello Scaffold rimane
attivo e navigabile.

ExpansionPanel
Il widget expansionPanel consente di creare schede
estendibili (conosciute anche con il termine accordion). In
genere, un expansionPanel offre la possibilità di espandere
e contrarre un elemento tramite un bottone. In questo modo
è possibile accedere a un contenuto di dettaglio relativo al
panel.

Listato 7.4 main.dart


import 'package:flutter/material.dart';

void main() {
runApp(const MyApp());
}

class MyApp extends StatelessWidget {


const MyApp({Key? key}) : super(key: key);

@override
Widget build(BuildContext context) {
return MaterialApp(
// Hide the debug banner
debugShowCheckedModeBanner: false,
theme: ThemeData.light(),
home: const HomePage(),
);
}
}

class Item {
String name;
String description;
bool isExpanded;

Item({
required this.name,
required this.description,
this.isExpanded = false,
});
}

class HomePage extends StatelessWidget {


const HomePage({super.key});

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Example'),
),
body: const PanelList()
);
}
}

List<Item> generateItems(int numberOfItems) {


return List<Item>.generate(numberOfItems, (int index) {
return Item(
name: 'Panel $index',
description: 'Lorem ipsum dolor sit amet, consectetur
adipiscing elit, sed do eiusmod tempor incididunt ut labore et
dolore magna aliqua. Ut enim ad minim veniam, quis nostrud
exercitation ullamco laboris nisi ut aliquip ex ea commodo
consequat.',
);
});
}

class PanelList extends StatefulWidget {


const PanelList({super.key});
@override
State<PanelList> createState() => _PanelListState();
}
class _PanelListState extends State<PanelList> {
final List<Item> _items = generateItems(3);

@override
Widget build(BuildContext context) {
return Column(
children: <Widget>[
ExpansionPanelList(
expansionCallback: (int index, bool isExpanded) {
setState(() {
_items[index].isExpanded = !isExpanded;
});
},
children: _items.map((Item item) {
debugPrint('${item.isExpanded}');
return ExpansionPanel(
headerBuilder: (BuildContext context, bool isExpanded)
{
return ListTile(
title: Text(item.name)
);
},
body: Padding(padding: const EdgeInsets.all(16.0),
child: Text(item.description)),
isExpanded: item.isExpanded,
);
}).toList()
)
]
);
}
}

L’esempio in questione fa uso del widget


ExpansionPanelList, che a sua volta itera una lista per
creare gli ExpansionPanel. La lista iterata da
ExpansionPanelList corrisponde a 3 oggetti Item. Questo
processo ci consente di avere un oggetto per ogni panel
che ha la property isExpanded, che indica al widget
ExpansionPanel se il suddetto elemento debba essere
espanso o meno. Quando l’utente espande l’elemento,
viene invocata la callback expansionCallback, che setta la
property isExpanded dell’oggetto Item relativo e, di
conseguenza, il widget viene ricompilato e verrà espanso o
contratto.

Figura 7.12 ExpansionPanel.


Figura 7.13 ExpansionPanel attivo.

NOTA
Il widget ListTile è usato per mostrare un contenuto di solito
composto da un testo e un’icona con un’altezza fissa.

NOTA
ll metodo toList trasforma una mappa in una lista e in Flutter è
spesso usato per iterare una mappa e creare una lista di widget,
associata chiaramente a un widget tramite il parametro children.

SnackBar
Il widget snackBar in genere è usato per trasmettere un
messaggio all’utente a fronte di un’azione. Il messaggio
viene mostrato nel bottom.
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Example'),
),
body: Builder(
builder: (context) =>
Center(
child:
TextButton(onPressed: () {
final snackBar = SnackBar(
content: const Text('snackBar is opened!'),
action: SnackBarAction(
label: 'Close',
onPressed: () {
// Some code to undo the change.
},
),
);
ScaffoldMessenger.of(context).showSnackBar(snackBar);
}, child: const Text('open snackBar'))
)
)
);
}

Semplicemente, il widget in questione viene istanziato


tramite la classe SnackBar e tramite la property action
definiamo il widget SnackBarAction, che infine definisce
l’azione da compiere. ScaffoldMessenger viene usato per
interagire con Scaffold e di conseguenza mostrare lo
SnackBar.
Figura 7.14 SnackBar.

NOTA
Vi invito a provare a rimuovere il widget Builder (builder key)
presente nel widget Scaffold. Ciò che noterete, procedendo con
questa modifica, è che Scaffold non sarà presente nel context,
pertanto è necessario procedere con un nuovo builder per
arricchire il context del widget Scaffold.

Visualizzazione dati
Flutter offre un set di widget piuttosto ampio per
organizzare al meglio la visualizzazione dei dati.

Card
Le card sono elementi di UI che mostrano contenuti e
azioni relativi allo stesso topic. Le card vengono create
tramite la classe Card, e nel nostro esempio conterrà una
colonna con ListTile e una riga. L’esempio creato si avvicina
molto all’utilizzo che di solito si fa di questa tipologia di
widget.
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Example'),
),
body: Card(
margin: const EdgeInsets.all(12.0),
child: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
ListTile(
leading: const CircleAvatar(foregroundImage:
AssetImage('assets/images/cat.jpg')),
title: const Text.rich(
TextSpan(
text: 'Username: ', // default text style
children: <TextSpan>[
TextSpan(text: 'Walter', style:
TextStyle(fontWeight: FontWeight.bold)),
],
),
),
subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: const [
Text('Lingua: Italiano', textAlign:
TextAlign.left),
Text('Iscrizione: Novembre 2008', textAlign:
TextAlign.left),
]
),
),
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: <Widget>[
TextButton(
child: const Text('BACHECA'),
onPressed: () {},
),
],
),
],
),
),
);
}

Assets
CircleAvatar è di solito usato per mostrare l’immagine di
profilo di uno user; al suo interno facciamo uso del widget
AssetImage che consente di visualizzare un’immagine
presente nella nostra directory di assets.
La directory assets di norma viene creata nella root del
progetto e deve essere indicata nel file pubspec.yaml, insieme
a tutti i file che dovremo usare nell’applicazione. Il file
pubspec.yaml conterrà già un esempio commentato. Nel

nostro caso la porzione di file interessata sarà così


composta:
flutter:
uses-material-design: true
assets:
- assets/images/cat.jpg
Figura 7.15 Card con azione.

Chip
Il widget Chip è un piccolo elemento che mostra una
breve informazione e, in alcuni casi, può anche richiedere
un’interazione. Gli esempi più comuni sono l’uso di filtri di
ricerca, piuttosto che un elenco di tag.
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Example'),
),
body: Chip(
avatar: const CircleAvatar(foregroundImage:
AssetImage('assets/images/cat.jpg'), child: Text('AB')),
label: const Text('Messages'),
onDeleted: () {},
)
);
}
Figura 7.16 Chip.

DataTable
Il widget DataTable ci aiuta a organizzare i dati in una
tabella, composta chiaramente da righe e colonne.
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Pound for pound'),
),
body: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
DataTable(
columns: const <DataColumn>[
DataColumn(
label: Expanded(
child: Text(
'Player',
),
),
),
DataColumn(
label: Expanded(
child: Text('Ranking'),
),
),
],
rows: const <DataRow>[
DataRow(
cells: <DataCell>[
DataCell(Text('Vincenzo')),
DataCell(Text('1')),
],
),
DataRow(
cells: <DataCell>[
DataCell(Text('Ralf')),
DataCell(Text('2')),
],
),
DataRow(
cells: <DataCell>[
DataCell(Text('Nicolas')),
DataCell(Text('3')),
],
),
],
)
]
)
);
}

Gli elementi principali del widget DataTable sono


DataColumn e DataRow, rispettivamente utili per creare
colonne e righe. I dati vengono organizzati in celle
(intersezione tra colonna e righe) con DataCell.
Figura 7.17 DataTable.

GridView
Il widget GridView è un griglia scrollabile. È generalmente
utilizzata per mostrare i dati in una forma tabellare.
Vedremo più avanti un approfondimento importante.
GridView è un widget molto utilizzato nella costruzione di
layout che necessitano la visualizzazione di più dati.
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Grid'),
),
body: GridView.count(
padding: const EdgeInsets.all(5),
crossAxisSpacing: 5,
mainAxisSpacing: 5,
crossAxisCount: 2,
children: <Widget>[
Container(
padding: const EdgeInsets.all(4),
color: Colors.brown,
child: const Text('First...', style: TextStyle(color:
Colors.white)),
),
Container(
padding: const EdgeInsets.all(4),
color: Colors.deepPurple,
child: const Text('Second...', style: TextStyle(color:
Colors.white)),
),
Container(
padding: const EdgeInsets.all(4),
child: const Text('Third...'),
),
Container(
padding: const EdgeInsets.all(4),
color: Colors.orange,
child: const Text('Fourth...'),
),
Container(
padding: const EdgeInsets.all(4),
color: Colors.black,
child: const Text('Fifth...', style: TextStyle(color:
Colors.white)),
),
Container(
padding: const EdgeInsets.all(4),
color: Colors.red,
child: const Text('Sixth...', style: TextStyle(color:
Colors.white)),
),
],
)
);
}
Figura 7.18 GridView.

Tooltip
Il widget tooltip è utile per mostrare all’utente una precisa
informazione. Questo avviene “tappando” a lungo il
componente o passando il mouse sopra l’elemento (non
dimentichiamoci che Flutter è usato anche per la creazione
di applicazioni web).
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Pound for pound'),
),
body: const Tooltip(
message: 'I am a Tooltip',
child: Text('Hover over the text to show a tooltip.'),
)
);
}

Figura 7.19 Tooltip.

Stepper
Il widget Stepper visualizza lo step in cui l’utente si trova.
Nel caso, per esempio, di una registrazione o di una
compilazione di dati a step, un widget di questo tipo torna
estremamente utile.
class StepperPage extends StatefulWidget {
const StepperPage({super.key});
@override
State<StepperPage> createState() => _StepperPageState();
}

class _StepperPageState extends State<StepperPage> {


int _index = 0;
@override
Widget build(BuildContext context) {
return Stepper(
currentStep: _index,
onStepCancel: () {
if (_index > 0) {
setState(() {
_index -= 1;
});
}
},
onStepContinue: () {
if (_index <= 0) {
setState(() {
_index += 1;
});
}
},
onStepTapped: (int index) {
setState(() {
_index = index;
});
},
steps: const <Step>[
Step(title: Text('Step One'), content: Text('Description
step One')),
Step(title: Text('Step Two'), content: Text('Description
step Two')),
],
);
}
}

Il widget Stepper usa un counter interno definito tramite


la property currentStep per renderizzare lo step corretto, a
sua volta definito nella lista tramite la property steps. Le
callback onStepCancel, onStepContinue e onStepTapped consentono
di navigare tra gli step, cambiando il valore di currentStep.
Figura 7.20 Stepper.

Bottoni
Flutter offre un set di bottoni di diversi design e con
diverse user experience.

DropdownMenu
Il widget DropdownMenu è utile per mostrare e dare
l’opportunità all’utente di scegliere un bottone da un elenco.

Listato 7.5 main.dart


import 'package:flutter/material.dart';

void main() {
runApp(const MyApp());
}

class MyApp extends StatelessWidget {


const MyApp({super.key});

@override
Widget build(BuildContext context) {
return MaterialApp(
// Hide the debug banner
debugShowCheckedModeBanner: false,
home: Scaffold(
appBar: AppBar(
title: const Text('DropdownMenu'),
),
body: const ButtonsPage()
)
);
}
}

class ButtonsPage extends StatefulWidget {


const ButtonsPage({super.key});

@override
State<ButtonsPage> createState() => _ButtonsPageState();
}

class _ButtonsPageState extends State<ButtonsPage> {

List <DropdownMenuItem<String>> items = ['Meat', 'Fish',


'Vegetable'].map<DropdownMenuItem<String>>((String value) {
return DropdownMenuItem<String>(
value: value,
child: Text(value),
);
}).toList();

late String? dropdownValue = items.first.value;

@override
Widget build(BuildContext context) {

return Center(
child: DropdownButton<String>(
value: dropdownValue,
icon: const Icon(Icons.arrow_downward),
onChanged: (String? value) {
setState(() {
dropdownValue = value!;
});
},
items: items
)
);
}
}

La classe DropdownButton renderizza la lista items, che


contiene a sua volta una lista di DropdownMenuItem.
dropdownValue verrà aggiornato con il valore contenuto nel

DropdownMenuItem.

Figura 7.21 DropdownMenu(1).


Figura 7.22 DropdownMenu(2).

PopupMenuButton
Il widget PopupMenuButton visualizza un menu
all’interno di un piccolo popup.
class PopupMenuPage extends StatefulWidget {
const PopupMenuPage({super.key});

@override
State<PopupMenuPage> createState() => _PopupMenuPageState();
}

class _PopupMenuPageState extends State<PopupMenuPage> {

@override
Widget build(BuildContext context) {
return Center(
child: PopupMenuButton<int>(
onSelected: (value) {},
itemBuilder: (BuildContext context) => <PopupMenuEntry<int>>
[
const PopupMenuItem<int>(
value: 1,
child: Text('Option 1'),
),
const PopupMenuItem<int>(
value: 2,
child: Text('Option 2'),
),
const PopupMenuItem<int>(
value: 3,
child: Text('Option 3'),
),
],
),
);
}
}

Il widget PopupMenuButton implementa una lista di


PopupMenuItem tramite un itemBuilder. Alla selezione di
uno dei PopupMenuItem viene chiamata la callback
onSelected di PopupMenuButton.

Figura 7.23 PopupMenuButton(1).


Figura 7.24 PopupMenuButton(2).

floatingActionButton, ElevatedButton
,IconButton,OutlinedButton,TextButton
floatingActionButton è un bottone “fluttuante”;
ElevatedButton è un bottone in rilievo; IconButton è un
bottone a icona; infine OutlinedButton differisce dal widget
TextButton per il fatto che il primo ha un bordo ed è di
conseguenza in rilievo.
Racchiudiamo i bottoni all’interno di un unico esempio.
Listato 7.6 main.dart
import 'package:flutter/material.dart';

void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});

@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
home: Scaffold(
appBar: AppBar(
title: const Text('Buttons example'),
),
floatingActionButton: FloatingActionButton(
onPressed: () {
// Add your onPressed code here!
},
child: const Icon(Icons.event_available_sharp),
),
body: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
const SizedBox(height: 30),
ElevatedButton(
onPressed: () {},
child: const Text('Enabled'),
),
IconButton(
iconSize: 62,
color: Colors.blue,
icon: const Icon(Icons.event_available_sharp),
onPressed: () {},
),
OutlinedButton(
onPressed: () {},
child: const Text('Enabled'),
),
TextButton(
style: TextButton.styleFrom(
textStyle: const TextStyle(fontSize: 18),
),
onPressed: () {},
child: const Text('Enabled'),
),
],
),
)));
}
}
Figura 7.25 Buttons.

Input
In questo paragrafo prendiamo in esempio i principali
widget da utilizzare per gestire gli input. I widget di questo
tipo sono fondamentali per gestire la user experience.
L’esempio a seguire implementa i seguenti widget: Radio,
CheckBox, Slider, Switch, TextField, TextButton e
DateTime.

Radio
Il widget Radio consente di scegliere un’“opzione” tra un
gruppo di possibilità. Il gruppo viene definito tramite la key
groupValue. Radio implementa una callback tramite la key

onChanged.

CheckBox
Il widget Checkbox consente di gestire un booleano. Il
valore del widget CheckBox può essere true o false e
questo viene settato tramite la key value. Anche questo
widget implementa una callback tramite la key onChanged.

Slider
Il widget Slider consente di selezionare un valore tra un
range più ampio. La key value consente di selezionare il
valore di default, mentre max e divisions compongono
rispettivamente un range con un valore massimo e gli step
di incremento definiti da divisions. Nell’ esempio con un
max 100 e un divisions 20, verrà creato uno slider con 5
possibilità: 0, 20, 40, 60 ,80 e 100.

Switch
Il widget in questione consente di definire uno stato:
on/off tramite un componente chiamato Switch.
Esattamente come il widget CheckBox, viene acceso o
spento tramite la key value e anche questo widget gestisce
la callback onChanged.

TextField
Il widget TextField consente di inserire dei dati
interagendo con la keyboard del device. Nell’esempio
proposto, simuliamo l’inserimento di un username e una
password. In questo caso la differenza tra i due input viene
gestita dall’oscuramento della password, feature gestita
dalla key obscureText. La chiave decoration invece consente di
definire l’aspetto dell’input box e questo viene fatto tramite
la classe InputDecoration, che consente di implementare
diverse feature; nel nostro caso la usiamo per inserire un
bordo e una label.

DatePicker
Il date picker è un componente fondamentale nella
gestione delle date. Normalmente viene usato per
selezionare una data o un range di date.
Il componente può essere mostrato tramite la funzione
showDatePicker e viene gestito tramite Future.

showDatePicker consente di configurare il date picker


tramite diversi parametri, tra i quali: initialDate, che mostra
la data di default, firstDate e lastDate, che corrispondono
all’inizio e alla fine delle date selezionabili, initialEntryMode,
che può configurare il data picker come griglia o testuale, e
altri parametri utili alla gestione del data picker.

Listato 7.7 main.dart


import 'package:flutter/material.dart';

void main() {
runApp(const MyApp());
}

class MyApp extends StatelessWidget {


const MyApp({super.key});

@override
Widget build(BuildContext context) {
return MaterialApp(
// Hide the debug banner
debugShowCheckedModeBanner: false,
home: Scaffold(
appBar: AppBar(
title: const Text('Input test'),
),
body: const InputPage()));
}
}

class InputPage extends StatefulWidget {


const InputPage({super.key});

@override
State<InputPage> createState() => _InputPageState();
}

class _InputPageState extends State<InputPage> {


DateTime selectedDate = DateTime.now();

bool isChecked = false;


bool? radioChecked = false;
double _currentSliderValue = 20;
bool switchChecked = false;
String? date;

@override
Widget build(BuildContext context) {
return Column(children: [
ListTile(
title: const Text('Accept'),
leading: Radio(
value: true,
groupValue: radioChecked,
onChanged: (value) {
setState(() {
radioChecked = value;
});
},
),
),
ListTile(
title: const Text('Decline'),
leading: Radio(
value: false,
groupValue: radioChecked,
onChanged: (value) {
setState(() {
radioChecked = value;
});
},
),
),
ListTile(
title: const Text('Accept'),
leading: Checkbox(
checkColor: Colors.white,
value: isChecked,
onChanged: (bool? value) {
setState(() {
isChecked = value!;
});
},
),
),
Slider(
value: _currentSliderValue,
max: 100,
divisions: 5,
label: _currentSliderValue.round().toString(),
onChanged: (double value) {
setState(() {
_currentSliderValue = value;
});
},
),
Switch(
value: switchChecked,
onChanged: (bool value) {
setState(() {
switchChecked = value;
});
},
),
Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
children: const [
TextField(
decoration: InputDecoration(
border: OutlineInputBorder(),
labelText: 'Username',
),
),
SizedBox(height: 10),
TextField(
obscureText: true,
decoration: InputDecoration(
border: OutlineInputBorder(),
labelText: 'Password',
),
)
],
)),
TextButton(
onPressed: () async {
DateTime? pickedDate = await showDatePicker(
context: context,
initialEntryMode: DatePickerEntryMode.calendarOnly,
initialDate: DateTime(2021),
firstDate: DateTime(2021),
lastDate: DateTime(2023),
);
setState(() {
date =

'${pickedDate?.day.toString()}-${pickedDate?.month.toString()}-${pi
ckedDate?.year.toString()}';
});
},
child: Text(date ?? 'Select date'),
),
]);
}
}
Figura 7.26 Inputs.
Figura 7.27 DatePicker. Nel widget viene mostrato il risultato del
DatePicker.
Capitolo 8

Scrollable Widget

Realizzare layout che siano funzionali a migliorare


l’esperienza d’uso dell’utente è un compito arduo. Il design
grafico si basa su una serie di tecniche e pattern che,
tecnicamente, consentono di realizzare l’idea, il mockup, in
codice e quindi in feature applicativa. Per quanto possa essere
banale, la possibilità di rendere uno spazio “scrollabile”
consente di realizzare interfacce accattivanti; date uno sguardo
alla documentazione di Material design per farvi un’idea:
https://m1.material.io/patterns/scrolling-techniques.html#scrolling-

techniques-behavior.

Scroll
I widget di questa categoria aiuteranno lo sviluppatore a
capire come gestire la fase di scroll. L’uso di widget che siano
scrollabili è pressoché un’attività mandatoria per creare layout
mobili.
Negli esempi introdotti fino a qui, avrete certamente notato
che cosa accade quando viene superata l’altezza o la larghezza
dello screen o di un widget che delimita la sua dimensione.
Facciamo un esempio.

Listato 8.1 main.dart


import 'package:flutter/material.dart';

void main() {
runApp(const MyApp());
}

class MyApp extends StatelessWidget {


const MyApp({super.key});

@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
home: Scaffold(
appBar: AppBar(
title: const Text('Overflow'),
),
body: const SizePage()));
}
}

class SizePage extends StatelessWidget {


const SizePage({super.key});

@override
Widget build(BuildContext context) {
final size = MediaQuery.of(context).size;
debugPrint('size width: ${size.width} height: ${size.height}');
return Container(
color: Colors.red,
height: size.height / 3,
width: size.width / 3,
child: Column(children: <Widget>[
SizedBox(height: size.height / 3),
const Text('Overflow!!')
]));
}
}

Come in Figura 8.1, il widget Column ha come primo child il


widget SizedBox che riempie lo spazio destinato al container del
100%; così facendo il widget Text verrà posizionato al di fuori
del sizing del genitore. Infatti, Flutter ci mostrerà che siamo in
overflow di un tot di pixel. Questo accade poichè l’elemento
genitore non è “scrollabile”. Vediamo come introdurre questo
nuovo elemento.
Figura 8.1 Overflow segnalato da Flutter.

SingleChildScrollView
Ammetto che questo esempio è un pò al limite, ma credo che
per chi viene dallo sviluppo web l’approccio in questione possa
tornare utile.
Come rendiamo un widget scrollable?
Nel nostro caso, possiamo iniziare ad approcciare
l’argomento usando il widget SingleChildScrollView.
class SizePage extends StatelessWidget {
const SizePage({super.key});

@override
Widget build(BuildContext context) {
final size = MediaQuery.of(context).size;
debugPrint('size width: ${size.width} height: ${size.height}');
return Container(
color: Colors.red,
height: size.height / 3,
width: size.width / 3,
child: SingleChildScrollView(
child: Column(children: <Widget>[
SizedBox(height: size.height / 3),
const Text('Overflow!!')
])));
}
}

Figura 8.2 SingleChildScrollView.

L’uso del widdget SingleChildScrollView ha reso il nostro


container “scrollable”. Questo widget è usato in contesti
piuttosto custom, visto che abbiamo già dei widget che sono per
loro natura scrollabili. Rimane comunque utile nel caso in cui
abbiamo un widget da voler rendere scrollabile.
In questo esempio facciamo uso dell’istruzione
MediaQuery.of(context).size che ci consente di accedere, tra le altre

cose, all’altezza e alla larghezza del nostro screen. Il widget


SingleChildScrollView definisce semplicemente un child. Il suo
utilizzo dovrebbe essere rilegato a spazi ridotti e casistiche in
cui l’utilizzo di un widget come ListView risulterebbe over-
engineered. Un esempio calzante potrebbe essere quello di una
modale di ridotte dimensioni.

ListView, GridView
I widget ListView e GridView (già presentati nel capitolo
precedente) sono probabilmente gli strumenti più semplici e
immediati da utilizzare quando abbiamo una lista di items da
mostrare in un contenitore scrollabile.

Listato 8.2 main.dart


import 'package:flutter/material.dart';

void main() {
runApp(const MyApp());
}

class MyApp extends StatelessWidget {


const MyApp({super.key});

@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
home: Scaffold(
appBar: AppBar(
title: const Text('ListView'),
),
body: const ListViewPage()));
}
}

class ListViewPage extends StatelessWidget {


const ListViewPage({super.key});

@override
Widget build(BuildContext context) {
final size = MediaQuery.of(context).size;
return ListView(
children: <Widget>[
Container(
decoration: BoxDecoration(border: Border.all()),
height: size.height / 3,
child: const Center(child: Text('Entry A')),
),
Container(
decoration: BoxDecoration(border: Border.all()),
height: size.height / 3,
child: const Center(child: Text('Entry B')),
),
Container(
decoration: BoxDecoration(border: Border.all()),
height: size.height / 3,
child: const Center(child: Text('Entry C')),
),
],
);
}
}

Figura 8.3 ListView.


Come vediamo nel sorgente appena presentato, l’uso del
widget ListView può risultare piuttosto semplice; nonostante ciò
ListView, così come altri widget, può essere utilizzato e
istanziato con più di un costrutto. Per esempio, Listview ha 4
costrutti: ListView, builder, custom e separated.
Ogni costrutto prevede chiaramente un suo caso d’uso
specifico. Il costruttore ListView lo abbiamo appena introdotto.
builder è utilizzato per costruire i widget figli ricorsivamente.
Listview.custom consente di personalizzare la creazione dei
widget figli tramite un SliverChildDelegate, che appunto può
consentire la creazione di un flusso di scrolling totalmente
custom. Si può pensare a ListView.builder come una versione
semplificata nata da ListView.custom. Infine, il costruttore
separated è utile per definire un separatore tra i widget figli.

@override
Widget build(BuildContext context) {
return ListView.separated(
separatorBuilder: (context, index) => const Divider(
color: Colors.black,
),
itemCount: 20,
itemBuilder: (context, index) => SizedBox(
height: 200,
child: Center(child: Text('Entry $index')),
));
}

Il widget Divider consente, tramite la callback


separatorBuilder, di dividere gli elementi della lista con una linea
orizzontale.
NOTA
In molti widget è definibile la property shrinkWrap, in cui il
comportamento di default (ListView, GridView, PageView, ..) è quello di
espandersi il più possibile, nei limiti imposti dal genitore. shrinkWrap
settato a true obbliga ListView a utilizzare solo lo spazio necessario.
ListView.builder
Procediamo provando a modificare l’esempio che vede
ListView come protagonista, con ListView.builder.
@override
Widget build(BuildContext context) {
final size = MediaQuery.of(context).size;
return ListView.builder(
itemCount: 3,
itemBuilder: (context, position) {
return Container(
decoration: BoxDecoration(border: Border.all()),
height: size.height / 3,
child: Center(child: Text('Entry $position')),
);
},
);
}

Le properties itemCount e itemBuilder consentono di costruire in


modo iterativo i figli che compongono il widget ListView.

GridView
Possiamo pensare a GridView come a un’evoluzione di
ListView. GridView, grazie alla property crossAxisCount, consente
di definire una griglia e gestire la disposizione dei figli in righe
separate se questi superano il limite definito in crossAxisCount.

Listato 8.3 main.dart


import 'package:flutter/material.dart';

void main() {
runApp(const MyApp());
}

class MyApp extends StatelessWidget {


const MyApp({super.key});

@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
theme: ThemeData.light(),
home: Scaffold(
appBar: AppBar(
title: const Text('Grid'),
),
body: GridView.count(
crossAxisCount: 1,
children: <Widget>[
Container(
decoration: BoxDecoration(border: Border.all()),
child: const Text('First...')),
Container(
decoration: BoxDecoration(border: Border.all()),
child: const Text('Second...')),
Container(
decoration: BoxDecoration(border: Border.all()),
child: const Text('Third...'),
),
],
)));
}
}

Nell’esempio raffigurato in Figura 8.4, disponiamo un figlio per


riga, nulla ci vieta di costruire griglie più complesse,
incrementando il valore di crossAxisCount.
GridView ha più costrutti: GridView, builder, custom, count ed
extent. Nell’esempio appena presentato abbiamo fatto uso del
costrutto count, che credo sia il più utilizzato per il widget
GridView. Questo consente di definire una griglia partendo già
da un numero di elementi prefissati lungo l’asse trasversale
(orizzontale): crossAxisCount.
Rispetto a quanto presentato, avremmo potuto fare uso di
List.generate per generare ricorsivamente i nostri widget,

esattamente come visto per ListView.


body: GridView.count(
crossAxisCount: 1,
children: List.generate(3, (index) {
return Container(
decoration: BoxDecoration(border: Border.all()),
child: Text('$index...'));
}))

Il costruttore GridView.extent consente di gestire facilmente la


grandezza dei figli lungo l’asse trasversale tramite la property:
maxCrossAxisExtent.

Come abbiamo visto i widget ListView e GridView, nonostante


possano sembrare utili a uno stesso scopo, hanno una gestione
dei figli molto diversa. ListView ha un ordine dei figli orizzontale
o verticale, una gestione simile a un elenco. GridView, invece,
può gestire un elenco più complesso, ordinando elementi in
righe e colonne.
Entrambi implementano diversi costrutti e tantissimi parametri
per creare ogni genere di layout che potrebbe venire in mente.
Figura 8.4 GridView.

PageView
Il widget PageView consente di gestire all’interno di un unico
widget più pagine alle quali si può accedere tramite gesture.
Esattamente come l’uso delle TabBar, è necessaria la presenza
di un controller per coordinare l’accesso alle differenti pagine.
La pagina selezionata dall’utente sarà quella mostrata dalla
view.

Listato 8.4 main.dart


import 'package:flutter/material.dart';

void main() {
runApp(const MyApp());
}

class MyApp extends StatelessWidget {


const MyApp({super.key});

@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
home: Scaffold(
appBar: AppBar(
title: const Text('PageView'),
),
body: const PageViewExample()));
}
}

class PageViewExample extends StatelessWidget {


const PageViewExample({super.key});

@override
Widget build(BuildContext context) {
final PageController controller = PageController();
return PageView(
scrollDirection: Axis.horizontal,
controller: controller,
children: const <Widget>[
Center(
child: Text('Page One'),
),
Center(
child: Text('Page Two'),
),
Center(
child: Text('Page Three'),
),
],
);
}
}
Oltre a una serie di parametri che rendono il widget
personalizzabile, scrollDirection, su tutti, consente di definire il
comportamento di accesso alle pagine tramite una gesture
orizzontale piuttosto che verticale.

CustomScrollView
Il widget CustomScrollView è apparso già nel capitolo
precedente, utilizzato nell’esempio fornito per la SliverAppBar.
CustomScrollView fornisce un supporto per realizzare
elementi scrollabili con effetti che si basano sugli sliver. L’effetto
di scrolling è realizzabile attraverso l’uso di tre differenti slivers:
SliverAppBar, SliverList, e SliverGrid. Gli slivers devono essere

utilizzati tramite il parametro slivers, che è appunto una lista.


Nell’esempio a seguire faremo uso esclusivo del widget
SliverGrid, al contrario di quanto fatto nella presentazione dello
SliverAppBar, in cui abbiamo fatto uso di due sliver.
SliverGrid e SliverList sono simili a GridView e ListView, con
la differenza che i primi vengono usati all’interno di una
ScollView, CustomScrollView in questo caso, mentre i secondi
derivano comunque dagli sliver. Utilizzare SliverGrid e SliverList
invece che GridView e ListView consente di avere un controllo
avanzato del comportamento di scroll; diversamente, non
avrebbe nessun valore aggiunto.

Listato 8.5 main.dart


import 'package:flutter/material.dart';

void main() {
runApp(const MyApp());
}

class MyApp extends StatelessWidget {


const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
home: Scaffold(
appBar: AppBar(
title: const Text('Example'),
),
body: const CustomScrollViewPage()));
}
}

class CustomScrollViewPage extends StatefulWidget {


const CustomScrollViewPage({super.key});

@override
State<CustomScrollViewPage> createState() =>
_CustomScrollViewPageState();
}

class _CustomScrollViewPageState extends State<CustomScrollViewPage> {


@override
Widget build(BuildContext context) {
return CustomScrollView(
slivers: [
SliverGrid(
gridDelegate: SliverGridDelegateWithMaxCrossAxisExtent(
maxCrossAxisExtent: MediaQuery.of(context).size.width / 2,
),
delegate: SliverChildBuilderDelegate(
(BuildContext context, int index) {
return Container(
decoration: BoxDecoration(border: Border.all()),
alignment: Alignment.center,
child: Text('Entry $index'),
);
},
childCount: 40,
),
)
],
);
}
}
Figura 8.5 CustomScrollView.

Nell’esempio appena proposto, SliverGrid utilizza un delegato


per la costruzione dei figli che compongono la griglia. Il
parametro usato, oltre a delegate, è gridDelegate, che consente di
definire size e posizione.
maxCrossAxisExtent: MediaQuery.of(context).size.width / 2 consente
di definire due figli per riga.
DraggableScrollableSheet
Il widget DraggableScrollableSheet è usato per creare una
porzione di layout che sia scrollabile e che si possa ingrandire o
restringere attraverso una gesture, drag, che tradotto significa
“trascinare” (suggerisce qual è il comportamento del widget in
oggetto). DraggableScrollableSheet ancora al bottom dello
screen un widget che, tramite gesture, è possibile estendere
fino a riempire tutto lo spazio disponibile.

Listato 8.6 main.dart


import 'package:flutter/material.dart';

void main() {
runApp(const MyApp());
}

class MyApp extends StatelessWidget {


const MyApp({super.key});

@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
home: Scaffold(
appBar: AppBar(
title: const Text('Example'),
),
body: const DraggableScrollable()));
}
}

class DraggableScrollable extends StatefulWidget {


const DraggableScrollable({super.key});

@override
State<DraggableScrollable> createState() =>
_DraggableScrollableState();
}

class _DraggableScrollableState extends State<DraggableScrollable> {


@override
Widget build(BuildContext context) {
return DraggableScrollableSheet(
initialChildSize: .2,
minChildSize: .1,
maxChildSize: .5,
builder: (BuildContext context, ScrollController scrollController)
{
return Container(
color: Colors.blue[300],
child: ListView.builder(
controller: scrollController,
itemCount: 25,
itemBuilder: (BuildContext context, int index) {
return ListTile(title: Text('Entry $index'));
},
),
);
},
);
}
}
Figura 8.6 DraggableScrollableSheet.

I parametri initialChildSize, minChildSize e maxChildSize


definiscono il comportamento del widget. Il primo parametro
definisce in proporzione (0-1), rispetto al widget contenitore,
l’altezza al rendering del widget; minChildSize e maxChildSize,
invece, l’estensione minima e massima del widget.
NestedScrollView
Il widget NestedScrollView è usato quando abbiamo
necessità di strutturare il layout innestando più widget
scrollabili senza dover scrivere codice troppo complesso. Un
caso d’uso reale è quello, ad esempio, di utilizzare la TabBar
con una SliverAppBar, che consenta quindi al nostro layout
uno scroll sia verticale che orizzontale.

Listato 8.7 main.dart


import 'package:flutter/material.dart';

void main() {
runApp(const MyApp());
}

class MyApp extends StatelessWidget {


const MyApp({super.key});

@override
Widget build(BuildContext context) {
return const MaterialApp(
debugShowCheckedModeBanner: false,
home: Scaffold(body: NestedScrollViewPage()));
}
}

class NestedScrollViewPage extends StatefulWidget {


const NestedScrollViewPage({super.key});

@override
State<NestedScrollViewPage> createState() =>
_NestedScrollViewPageState();
}

class _NestedScrollViewPageState extends State<NestedScrollViewPage>


with TickerProviderStateMixin {
late TabController _tabController;

@override
void initState() {
super.initState();
_tabController = TabController(vsync: this, length: 3);
}

@override
Widget build(BuildContext context) {
return NestedScrollView(
headerSliverBuilder: (BuildContext context, bool
innerBoxIsScrolled) {
return <Widget>[
SliverAppBar(
pinned: true,
snap: true,
floating: true,
bottom: TabBar(
tabs: const [
Tab(text: 'Tab 1'),
Tab(text: 'Tab 2'),
Tab(text: 'Tab 3'),
],
controller: _tabController,
)),
];
},
body: TabBarView(
controller: _tabController,
children: <Widget>[
Center(
child: ListView.builder(
itemCount: 25,
itemBuilder: (BuildContext context, int index) {
return ListTile(
title: Text('Tile ${index + 1}'),
);
},
),
),
const Center(
child: Text('Hello World'),
),
const Center(
child: Text('Hello World'),
),
],
));
}
}

Il widget NestedScrollView si basa sull’uso della property


headerSliverBuilder. Quest’ultima è usata per creare una

SilverAppBar che gestisce una TabBar. headerSliverBuilder, in


questo modo, consentirà di gestire lo scroll orizzontale. La
property body, invece, definisce il contenuto del widget
NestedScrollView, che implementa lo scroll verticale: la
TabBarView.
Figura 8.7 NestedScrollView.

NotificationListener
Il widget NotificationListener serve a catturare gli eventi
generati dalle operazioni di scroll. Modifichiamo l’esempio
precedente, utilizzando il widget NotificationListener per
“wrappare” il widget scrollable.
children: <Widget>[
Center(
child: NotificationListener<ScrollNotification>(
onNotification: (scrollState) {
debugPrint('scrollState: $scrollState');
return false;
},
child: ListView.builder(
itemCount: 25,
itemBuilder: (BuildContext context, int index) {
return ListTile(
title: Text('Tile ${index + 1}'),
);
},
)),
),

Possiamo immediatamente notare dai log che, quando


interagiamo con un widget scrollable, verrà prodotto un log
puntuale:
"flutter: scrollState: UserScrollNotification(depth: 0 (local),
FixedScrollMetrics(0.0..[812.0]..672.0), direction:
ScrollDirection.reverse)

flutter: scrollState: ScrollUpdateNotification(depth: 0 (local),


FixedScrollMetrics(15.0..[812.0]..657.0), scrollDelta: 15.0,
DragUpdateDetails(Offset(0.0, -15.0)))"

Il log prodotto è generato dall’evento onNotification. È


possibile accedere alle informazioni di dettaglio dell’evento
per poter introdurre delle logiche.

Scrollbar
Il widget Scrollbar nasce per modificare il comportamento
di default dello scroll di Flutter. Quando un utente
interagisce con un widget scrollabile, lo fa tramite una barra
di scorrimento che scompare appena dopo la conclusione
della fase di scroll.
Questo comportamento, come altri aspetti legati alla
barra dello scroll, può essere modificato tramite il widget
Scrollbar.
return Scrollbar(
thumbVisibility: true,
thickness: 15,
child: ListView.builder(
itemCount: 25,
itemBuilder: (BuildContext context, int index) {
return ListTile(
title: Text('Tile ${index + 1}'),
);
},
),
);

Il parametro thumbVisibility definisce se la barra di scroll


debba essere visibile anche quando lo scroll non è attivo;
nel nostro caso, con true, accade esattamente questo. Il
parametro thickness indica i pixel della barra di scroll, che di
default è di 3px.
Figura 8.8 Scrollbar.

ScrollController
Abbiamo già utilizzato, negli esempi pregressi, il
controller ScrollController. Spendiamo due parole per
descrivere effettivamente che cosa sia. La classe
ScrollController è usata dai widget scrollable per gestire lo
scrolling di ogni singolo widget. Un controller può essere
usato per gestire più widget. Nel caso in cui, invece,
utilizzassimo un controller per leggere lo stato dell’offset di
scorrimento, questo deve essere specifico per il singolo
widget; infatti, il widget ScrollController altro non è che un
listener. Ciò che precedentemente abbiamo fatto con
ScrollNotification può essere fatto anche tramite il
controller.
Nell’esempio in cui abbiamo fatto uso del widget
DraggableScrollableSheet, abbiamo necessariamente
dovuto impostare uno ScrollController per il widget
ListView, anche se, come visto in altri casi, non è
mandatorio. In quel caso è necessario poiché, usando due
widget scrollabili, è necessario impostare il controller in
quanto si potrebbe incappare in errori di gestione dello
scroll.
NOTA
Un esercizio potrebbe essere quello di rimuovere il controller
nell’esempio del widget DraggableScrollableSheet per rendersi
conto di quanto sia fondamentale la comunicazione tra widget
innestati che necessitano di un controller per comunicare.

Procediamo con un esempio che vede ScrollController


lavorare come listener.
late ScrollController _controller;

void _onScrollEvent() {
debugPrint("Scroll event: ${_controller.position}");
}

@override
void initState() {
_controller = ScrollController();
_controller.addListener(_onScrollEvent);
super.initState();
}

@override
Widget build(BuildContext context) {
return ListView.builder(
controller: _controller,
itemCount: 25,
itemBuilder: (BuildContext context, int index) {
return ListTile(
title: Text('Tile ${index + 1}'),
);
},
);
}

"flutter: Scroll event:


ScrollPositionWithSingleContext#3d437(offset: 297.6, range:
0.0..728.0, viewport: 706.0, ScrollableState, BouncingScrollPhysics
-> RangeMaintainingScrollPhysics,
DragScrollActivity#2a638(ScrollDragController#cad2a),
ScrollDirection.reverse)
flutter: Scroll event:
ScrollPositionWithSingleContext#3d437(offset: 304.6, range:
0.0..728.0, viewport: 706.0, ScrollableState, BouncingScrollPhysics
-> RangeMaintainingScrollPhysics,
DragScrollActivity#2a638(ScrollDragController#cad2a),
ScrollDirection.reverse)"

ReorderableListView
Il widget ReorderableListView consente, oltre a garantire
lo scrolling delle informazioni contenute nel widget
ReorderableListView, il drag and drop degli elementi in
modo da poterne modificare l’ordinamento. Procediamo con
un esempio.
Listato 8.8 main.dart
import 'package:flutter/material.dart';

void main() {
runApp(const MyApp());
}

class MyApp extends StatelessWidget {


const MyApp({super.key});

@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
home: Scaffold(
appBar: AppBar(
title: const Text('Example'),
),
body: const ReorderableListPage()));
}
}

class ReorderableListPage extends StatefulWidget {


const ReorderableListPage({super.key});

@override
State<ReorderableListPage> createState() =>
_ReorderableListPageState();
}

class _ReorderableListPageState extends State<ReorderableListPage>


{
final List<int> _items = Iterable<int>.generate(10).toList();

void _reorderInt(int oldindex, int newindex) {


setState(() {
if (newindex > oldindex) {
newindex -= 1;
}
final items = _items.removeAt(oldindex);
_items.insert(newindex, items);
});
}

@override
Widget build(BuildContext context) {
return ReorderableListView(
onReorder: _reorderInt,
children: <Widget>[
for (int index = 0; index < _items.length; index += 1)
Padding(
key: Key('$index'),
padding: const EdgeInsets.all(16.0),
child: ListTile(
key: Key('$index'),
shape: RoundedRectangleBorder(
side: const BorderSide(color: Colors.black, width:
1),
borderRadius: BorderRadius.circular(5),
),
title: Text('Tile ${_items[index]}'),
),
),
],
);
}
}

Figura 8.9 ReorderableListView.


Figura 8.10 ReorderableListView(2).
Figura 8.11 ReorderableListView(3).

Il widget ReorderableListView deve implementare una


funzione di ordinamento, che nel nostro caso è _reorderInt.
Questa funzione riordina la lista _items, usata per mostrare
l’ordinamento all’interno dell’array di widget.
In questo esempio, abbiamo utilizzato un altro approccio
rispetto ai precedenti. Abbiamo utilizzato un ciclo for, per
costruire i widget figli di ReorderableListView, ma avremmo
anche potuto utilizzare ReorderableListView.builder, piuttosto
che List.generate o forEach, e altri ancora. Il consiglio è di
usare l’approccio con il quale avete maggiore
dimestichezza.
Capitolo 9

Layout Widget

I widget che consentono allo sviluppatore di strutturare al


meglio il layout e di conseguenza arricchiscono gli altri widget di
nuove proprietà sono enormemente importanti. I widget di tipo
layout sono, come già definiti in precedenza, widget invisibili, e
sono di due tipi: Single e Multiple child.

Vincoli
Prima di procedere all’analisi di widget per widget, vorrei
soffermarmi sulla gestione dei vincoli in Flutter, ciò è importante
che venga appreso e fatto suo.
I vincoli in Flutter sono 4 proprietà: altezza massima e
minima, larghezza massima e minima. I vincoli in questione
vengono comunicati al widget figlio e il figlio comunicherà al
padre la sua grandezza e altezza nell’assoluto rispetto dei
vincoli. Infine, il genitore decide la posizione del figlio nell’asse x
e y.
Partendo da questa regola, possiamo confermare che il
widget figlio non può scegliere la sua dimensione, se non entro i
vincoli, e la sua posizione in autonomia. La complicazione
avviene proprio sulla base che i vincoli vengono ereditati e ciò
significa che per calcolare la precisa dimensione di ogni widget
è necessario consultare tutto l’albero dei widget.
Là dove il widget esprime un size preciso, deve rispettare i
vincoli e allo stesso tempo il genitore deve aver pensato già a
un allineamento poiché, se l’allineamento non fosse definito, la
volontà del widget figlio potrebbe essere ignorata; se vi
ricordate, è quanto già preso in esame per il widget Container.
Quando definiamo una grandezza specifica, almeno nei
widget che lo consentono, di fatto stiamo utilizzando quello che
viene definito tight constraint, ovvero un vincolo stretto. Se
ispezioniamo il widget Container, vedremmo che quando
definiamo un altezza o una larghezza, Flutter crea un widget
figlio chiamato ConstrainedBox proprio per imporre al figlio il
vincolo che corrisponde essere l’altezza o la larghezza definita
in Container. Faremo un approfondimento su questo tema
qualche capitolo più avanti.
Rimane comunque a carico del widget figlio definire la propria
dimensione e questa logica può variare da widget a widget,
pertanto è fondamentale consultare la documentazione ufficiale
per comprendere a fondo la specifica implementativa. In genere
i comportamenti sono raggruppabili in tre categorie: i widget che
si espandono il più possibile, quelli che hanno una dimensione
particolare di default e quelli che ereditano la grandezza dai figli.
Rimane comunque valido quanto appena detto, anche perchè i
widget che possono utilizzare una logica di default, questa
potrebbe cambiare in base alla sua configurazione.
Vi invito a utilizzare un’applicazione creata dal team Flutter
con lo specifico scopo di far comprendere al meglio la gestione
dei vincoli, è reperibile all’indirizzo:
https://github.com/marcglasberg/flutter_layout_article.

Chiarita la questione vincoli, proseguiamo con lo studio dei


Layout widget, fondamentali per armonizzare i nostri layout.
Single-child
Il concetto è molto banale, per widget single-child si intende
che il widget layout arricchirà effettivamente un solo figlio.

Container
Abbiamo già trattato il widget Container nel Capitolo 5, ed è
certamente il capostipite dei widget layout.
Il suo costrutto è:
Container(
{Key? key,
AlignmentGeometry? alignment,
EdgeInsetsGeometry? padding,
Color? color,
Decoration? decoration,
Decoration? foregroundDecoration,
double? width,
double? height,
BoxConstraints? constraints,
EdgeInsetsGeometry? margin,
Matrix4? transform,
AlignmentGeometry? transformAlignment,
Widget? child,
Clip clipBehavior = Clip.none}
)

Analizziamo brevemente i parametri, affinché possiamo


renderci immediatamente conto di quanti aspetti ruotano intorno
al design del layout in Flutter.
La property aligment può essere gestita tramite la classe
Alignment, che offre costanti che possono essere usate per
posizionare con semplicità il figlio. I valori che possono essere
utilizzati sono: topLeft, topCenter, topRight, centerLeft, center,
centerRight, bottomLeft, bottomCenter e bottomRight. Il parametro
padding consente di definire un padding senza necessità di
usare il suddetto (Padding) widget.
Color è una property che consente di definire un background
color al container. Questo potrà essere gestito tramite la classe
Color. La classe Color è molto ampia e consente di utilizzare
diverse interfacce per diversi formati. È anche possibile
utilizzare la classe Colors, che consente di accedere
velocemente a un set di colori Material.
Per esempio, lo stesso background potremmo impostarlo in
diversi modi.
color: const Color.fromARGB(0xFF, 0x29, 0xB6, 0xF6),

piuttosto che
color: Colors.lightBlue,

e non solo.

BoxDecoration
La property decoration è utilizzata per decorare il container e
questo viene fatto tramite la classe Decoration. Finora, abbiamo
utilizzato BoxDecoration per applicare un bordo.
Per esempio: decoration: BoxDecoration(border: Border.all()),
BoxDecoration può far uso di queste properties: color, image,
border, borderRadius, gradient, backgroundBlendMode e shape.
Color, come per la properties del widget Container, definisce
un colore di background, image consente di applicare un’
immagine come background, border e borderRadius gestiscono i
bordi e l’arrotondamento degli angoli, Gradient gestisce un
gradiente, backgroundBlendMode consente di applicare un filtro al
background o al gradiente e infine shape consente di applicare
una particolare forma (rettangolare o circolare).
NOTA
Con il termine gradiente si intende una trasformazione graduale tra
due o più colori.
decoration: BoxDecoration(
image: const DecorationImage(
image: NetworkImage(

'https://www.apogeonline.com/contrib/uploads/9788850334063_quarta.jpg'),
fit: BoxFit.fill,
),
border: Border.all(color: Colors.black),
borderRadius: const BorderRadius.all(Radius.circular(12)),
boxShadow: const [
BoxShadow(
color: Colors.black,
offset: Offset(8.0, 8.0),
blurRadius: 10.0,
spreadRadius: 1.0,
), //BoxShadow
],
shape: BoxShape.rectangle),

In questo esempio abbiamo fatto uso di un’immagine,


prelevata magistralmente dal sito apogeonline, di un libro di
tutto rispetto (attenzione all’autore). Abbiamo applicato un bordo
e un border radius. Infine, abbiamo utilizzato la classe
BoxShadow per applicare un’ombra. I parametri color, offset,
blurRadius e spreadRadius indicano rispettivamente: il colore
dell’ombra, la posizione per la proiezione dell’ombra, la
sfocatura gaussiana e, infine, la “quantità” di ombra.
NOTA
BoxFit è un enum che consente di definire l’adattamento di
un’immagine in un dato spazio. Le possibilità sono: fill, contain, cover,
fitHeight, fitWidth, none e scaleDown. Fill adatta l’immagine allo spazio,
distorcendola pur di coprire del tutto lo spazio disponibile. Contain

rispetta le proporzioni dell’immagine, nonostante provi a coprire tutto lo


spazio disponibile. Cover adatta l’immagine, coprendo tutto lo spazio e
mantenendo le proporzioni, ma scalando l’immagine ed effettuando
tagli se necessario. FitHeight e fitWidth impongono che l’immagine
venga visualizzata per tutta l’altezza o la larghezza, senza
preoccuparsi dell’overflow dell’altezza o larghezza opposta. Infine,
scaleDown centra l’immagine ed effettua uno scaling per adattare

l’immagine allo spazio mantenendone le proporzioni.

Il prodotto di questo esempio è visibile nella Figura 9.1.

Figura 9.1 Esempio BoxDecoration.

La property foregroundDecoration, a differenza di decoration,


applica la decorazione sopra i children, non sotto. Il che
vorrebbe dire che, se applicassimo un foregroundDecoration
con uno sfondo di qualsiasi colore, e avessimo come figlio un
Text, questo non sarebbe visibile.
NOTA
Se qualcuno si sta chiedendo come rendere visibile un widget coperto
da un colore definito nel foregroundDecoration, questo può essere
fatto applicando un’opacità (Colors.blue.withOpacity(0.8)).

Le properties width, height, constraints e margin definiscono


rispettivamente: larghezza e altezza del container, eventuali
vincoli (per esempio altezza minima, larghezza massima, ecc.)
e i margini.
NOTA
Per margine si intende lo spazio vuoto tra i widget; nel dettaglio
possiamo immaginarlo come lo spazio vuoto tra i bordi di un widget e il
widget adiacente.

La property transform consente di applicare una trasformazione


prima del rendering. Queste logiche possono essere applicate
tramite la classe Matrix4, che offre diversi metodi per
manipolare gli oggetti. transformAlignment consente di allineare il
widget rispetto alla sua trasformazione. Questa property può
essere usata solo se è presente una trasformazione.
Child è il widget figlio. Infine, clipBehavior definisce il
comportamento di un’eventuale taglio del widget. Nel caso un
figlio sfori lo spazio disponibile, il comportamento di default è
quello di non applicare nessun taglio; è possibile, tramite la
property clipBehavior, applicare un taglio tramite tre valori:
hardEdge, antiAlias e e antiAliasWithSaveLayer. Il primo taglio è il
meno dispendioso in termini di risorse, il secondo applica al
ritaglio un effetto anti-aliased e infine l’ultimo migliora l’effetto
anti-aliased, gestendo l’eventuale differenza di colori tra
l’oggetto ritagliato e il colore di sfondo. È comunque sconsigliato
il suo utilizzo, in quanto è estremamente dispendioso.
return Center(
child: Container(
width: 100,
height: 100,
transformAlignment: Alignment.bottomCenter,
decoration: BoxDecoration(border: Border.all()),
transform: Matrix4.rotationZ(0.5),
alignment: Alignment.center,
clipBehavior: Clip.none,
child: Transform.scale(scale: 2, child: const Text('Hello
World'))),
);

Figura 9.2 ClipBehaviour.

NOTA
L’anti aliasing è una tecnica che consente di migliorare un frame
arrotondando o colmando un difetto grafico tramite pixel aggiuntivi.

Sizedbox
Il widget Sizedbox è molto simile al widget Container. A
differenza di Container, non è possibile gestire, per esempio,
colori o bordi. Il widget Sizedbox è sicuramente meno pesante
da gestire da parte del framework e il fatto che abbia un
costrutto di tipo const consente una maggiore performance in
fase di runtime, in quanto Flutter non dovrà ricompilarlo.
return const SizedBox(
width: 200, height: 200, child: Text('Hello World'));

Center
Abbiamo già usato decine di volte il widget Center, ma non è
stato mai introdotto come si deve. Il widget Center è usato per
allineare il figlio al centro dello spazio disponibile. Se non ha le
property heightFactor e widthFactor, di default si espanderà per
catturare tutto lo spazio disponibile.
return Center(
child: Container(
decoration:
BoxDecoration(border: Border.all()),
child:
const Text('Hello World')));

In questo esempio abbiamo un Container con un Text. L’uso


del widget Center farà in modo di porre al centro il Container
che, non avendo un sizing esplicitato, erediterà la grandezza del
widget, in questo caso una stringa di testo.
Figura 9.3 Center.

Inspector
Non dimentichiamoci che Flutter offre un potente inspector,
che VS Code implementa direttamente e mostra durante il run
dell’applicazione.
In Figura 9.4 possiamo vedere l’inspector che mostra il layout
del nostro widget tree. È intuibile che 812px e 375px siano le
dimensioni dello screen del device. Selezionando nell’albero il
widget Center vediamo direttamente il dettaglio del widget: in
questo caso il sizing corrisponde a 706x375px. La differenza tra
l’altezza di 812 e quella di 706, che corrisponde al widget
Center, è esattamente l’altezza del widget AppBar.
Per avere una corrispondenza visiva tra il widget selezionato
nell’inspector e il nostro simulatore o il device fisico, possiamo
attivare la feature Select Widget Mode, come in Figura 9.5.
È possibile delimitare la grandezza del widget Center grazie
alle proprietà height e width factor. Procediamo con un esempio
per vedere come funziona.
return Center(
heightFactor: 2,
widthFactor: 2,
child: Container(
height: 150,
width: 150,
decoration: BoxDecoration(border: Border.all()),
child: const Text('Hello World')));

In questo esempio abbiamo definito una grandezza del


Container di 150×150. L’uso delle property heightFactor e
widthFactor consente al widget Center di espandersi fino al
moltiplicarsi del sizing del figlio per i valori del factor del widget
Center; ciò detto, avremo il widget center di 300x300 con il
Container di 150x150.
Figura 9.4 Vs Code Widget Inspector.

Figura 9.5 Select Widget Mode.

Padding
Il padding, che ricordiamo essere lo spazio vuoto presente tra
il contenuto e il suo confine, è definibile in Flutter tramite il
widget Padding.
return Padding(
padding: const EdgeInsets.fromLTRB(50, 50, 0, 0),
child: Container(
decoration: BoxDecoration(border: Border.all()),
child: const Padding(
padding: EdgeInsets.all(14), child: Text('Hello
World'))));

Il widget padding usa la classe EdgeInsets per gestire lo


spazio, che tecnicamente chiamiamo offset. Nel nostro
esempio, abbiamo fatto uso di due metodi distinti: fromLTRB e all.
Il primo inserisce l’offset per ogni singola direzione: left, top,
right e bottom; il secondo, invece, inserisce l’offset per tutte le
direzioni.

Figura 9.6 Padding.

Align
Il widget Align segue la stessa logica del widget Center. È
usato quando vogliamo allineare un widget figlio non
esattamente al centro, come visto per l’omonimo widget. Il
widget Align consente, tramite la property alignment, di usare la
classe Alignment, come già mostrato nell’esempio legato al
container.
return Container(
height: 150,
decoration: BoxDecoration(border: Border.all()),
child: const Padding(
padding: EdgeInsets.all(14),
child: Align(
alignment: Alignment.centerRight, child: Text('Hello
World'))));

Figura 9.7 Align.

Se il widget Align ha dei vincoli, come in questo caso, e non


ha i parametri widthFactor e heighFactor valorizzati, si espanderà il
più possibile. Nel caso in cui non abbia vincoli e nessun fattore
di grandezza, Align sarà grande quanto la dimensione del figlio.
Nel caso in cui Align usi i fattori di grandezza, la grandezza
del widget Align verrà rapportata a quello del figlio. Questo
comportamento è rispettato poiché il widget Align non ha vincoli.
È bene memorizzare che i valori di grandezza che vengono
espressi, in questo caso dal Container, non riguardano il
Container stesso ma il figlio. Detto ciò, se il Container fosse di
500px, il widget Align sarebbe di 500px. Nel nostro esempio,
invece, il widget Align sarà di 300px.
return Container(
decoration: BoxDecoration(border: Border.all()),
child: const Align(
alignment: Alignment.centerRight,
widthFactor: 3,
child: SizedBox(width: 100),
));

AspectRatio
L’aspect ratio è la descrizione del rapporto tra larghezza e
altezza. L’esempio più classico di AspectRatio che può venire in
mente è il rapporto 16:9. Il widget in questione può definire la
grandezza del figlio esprimendo un rapporto.
return Container(
alignment: Alignment.centerRight,
height: 250.0,
width: 300.0,
decoration: BoxDecoration(border: Border.all()),
child: Padding(
padding: const EdgeInsets.all(14),
child: AspectRatio(
aspectRatio: 62 / 9,
child: Container(
decoration: BoxDecoration(border: Border.all())))));
Figura 9.8 AspectRatio.

Baseline
Il widget Baseline consente di posizionare un figlio usando
come logica la base del widget figlio. Procediamo con un
esempio per chiarire il concetto.
return Center(
child: Container(
height: 200.0,
width: 200.0,
decoration: BoxDecoration(border: Border.all()),
child: Baseline(
baseline: 0,
baselineType: TextBaseline.alphabetic,
child: Container(
width: 100,
height: 100,
decoration: BoxDecoration(border: Border.all())))));

Il parametro baseline impostato a 0 consente di posizionare la


base del widget figlio al top 0 del padre; questo perché
l’allineamento di default è impostato partendo dalle coordinate
top e left. Se avessimo aggiunto al Container un alignment, per
esempio Alignment.bottomright, avremmo ottenuto il risutato in
Figura 9.10. La property baselineType consente di scegliere la
tipologia di baseline da utilizzare. Le opzioni sono: alphabetic e
ideographic. Inutile dire che ideographic andrebbe usato nel caso
di caratteri ideografici.
Figura 9.9 Baseline.
Figura 9.10 Baseline(2).

ConstrainedBox
Il widget ConstrainedBox consente di impostare vincoli di
grandezza e altezza a un figlio. In un esempio in cui abbiamo
due container con entrambi i sizing definiti, se avvolgessimo il
Container figlio con un widget ContrainedBox, quest’ultimo ne
sarebbe senz’altro influenzato. Facciamo un esempio.
return Container(
decoration: BoxDecoration(border: Border.all()),
alignment: Alignment.center,
width: 250,
height: 250,
child: ConstrainedBox(
constraints: const BoxConstraints(
maxWidth: 150,
maxHeight: 150,
),
child: (Container(
width: 200,
height: 200,
decoration: BoxDecoration(border: Border.all()),
child: const Text('Hello World')))));

Il sizing del Container figlio sarà certamente di 150x150.


Provare per credere.

UnconstrainedBox e LimitedBox
Il widget LimitedBox ha una logica molto simile a
ConstrainedBox, con la differenza che quest’ultimo funziona
solo quando il genitore non ha nessun vincolo. Per poter
riprodurre quanto fatto per il widget precedente, è necessario
avvolgere LimitedBox con un widget di tipo UnconstrainedBox.
return Container(
decoration: BoxDecoration(border: Border.all()),
alignment: Alignment.center,
width: 250,
height: 250,
child: UnconstrainedBox(
child: LimitedBox(
maxWidth: 150,
maxHeight: 150,
child: (Container(
width: 200,
height: 200,
decoration: BoxDecoration(border: Border.all()),
child: const Text('Hello World'))))));

Transform
Il widget Transform consente di trasformare il widget prima
della sua fase di rendering.
La classe Transform può essere utilizzata direttamente con il
suo costruttore dove, grazie alla sua property transform, è
possibile usare la classe Matrix4. La suddetta classe consente
di applicare una trasformazione delle dimensioni: lunghezza,
larghezza e profondità. La classe Transform può essere
utilizzata, forse ancor più semplicemente, utilizzando i metodi
rotate, scale e translate.

Facciamo un breve esempio.


return Transform.scale(
scale: 5, child: const Center(child: Text('Hello World')));

In questo esempio, la property scale settata a 5 consente di


moltiplicare la grandezza del child per 5.

CustomSingleChildLayout
Il widget CustomSingleChildLayout crea un delegato per
gestire la posizione ed eventuali vincoli del widget. Può anche
essere usato per determinare il sizing del parent, di
conseguenza accedere ai vincoli.
Questo è il suo costrutto:
CustomSingleChildLayout({
Key key,
@required SingleChildLayoutDelegate delegate,
Widget child,
});

In questo esempio faremo uso di un delegato


SingleChildLayoutDelegate.
Listato 9.1 main.dart
import 'package:flutter/material.dart';

void main() {
runApp(const MyApp());
}

class MyApp extends StatelessWidget {


const MyApp({super.key});

@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
home: Scaffold(
appBar: AppBar(
title: const Text('Example'),
),
body: const ExamplePage()));
}
}

class ExamplePage extends StatelessWidget {


const ExamplePage({super.key});

@override
Widget build(BuildContext context) {
double parentMargin = 12;

return Container(
margin: EdgeInsets.all(parentMargin),
decoration: BoxDecoration(border: Border.all()),
alignment: Alignment.center,
width: 250,
height: 250,
child: CustomSingleChildLayout(
delegate: _MySingleChildLayoutDelegate(parentMargin),
child: Container(
decoration: BoxDecoration(border: Border.all()),
),
));
}
}

class _MySingleChildLayoutDelegate extends SingleChildLayoutDelegate {


_MySingleChildLayoutDelegate(this.parentMargin);

final double parentMargin;

@override
BoxConstraints getConstraintsForChild(BoxConstraints constraints) {
return BoxConstraints(
maxWidth: constraints.maxWidth / 2,
maxHeight: constraints.maxHeight / 2,
);
}

@override
Offset getPositionForChild(Size size, Size childSize) {
return Offset(parentMargin, parentMargin);
}

@override
bool shouldRelayout(_MySingleChildLayoutDelegate oldDelegate) {
return parentMargin != oldDelegate.parentMargin;
}
}
Figura 9.11 CustomSingleChildLayout.

L’uso del widget CustomSingleChildLayout si porta dietro


l’uso di un delegato e di conseguenza è necessario estendere la
classe SingleChildLayoutDelegate. Questo passo è mandatorio
proprio perché la classe in questione deve procedere con
l’override dei metodi: getConstraintsForChild, getPositionForChild e
shouldRelayout.

I metodi in questione consentono di definire dei vincoli. Nel


nostro esempio usiamo la classe BoxConstraints per impostare
di fatto la grandezza massima del widget child.
getPositionForChild, tramite la classe offset, imposta la posizione

top e left rispetto al widget child e infine shouldRelayout consente


di verificare una condizione per procedere nuovamente con il
build del widget. Questo è fondamentale nel caso in cui cambi
qualcosa nel layout e sia necessario cambiare vincoli e
grandezze. Nel nostro esempio ciò non può accadere in quanto
il margine impostato è cablato nel codice e non potrà mai
cambiare.

Expanded
Il widget Expanded, di cui abbiamo fatto già uso nei primissimi
capitoli, costringe un child dei widget Row, Column o Flex a
espandersi fino a utilizzare tutto lo spazio disponibile.
return Row(children: <Widget>[
Container(
decoration: BoxDecoration(border: Border.all()),
padding: const EdgeInsets.all(12),
child: const Text('1'),
),
Expanded(
child: Container(
decoration: BoxDecoration(border: Border.all()),
padding: const EdgeInsets.all(12),
child: const Text('2'),
)),
Container(
decoration: BoxDecoration(border: Border.all()),
padding: const EdgeInsets.all(12),
child: const Text('3'),
),
]);
Figura 9.12 Expanded.

FittedBox
FittedBox è un widget che consente di adattare le dimensioni
di un figlio a quelle del genitore. L’esempio proposto segue la
stessa logica dello “scale” che abbiamo già affrontato con il
widget Transform.scale. In questo caso lo “scale” e il
posizionamento sono gestiti autonomamente dal widget
FittedBox.
Container(
decoration: BoxDecoration(
border: Border.all(),
),
width: 180,
height: 30,
child: FittedBox(child: Text('Hello World')),
);

Figura 9.13 FittedBox.


FractionallySizedBox
Il widget FractionallySizedBox ha una logica simile al widget
precedente. L’uso di questo widget è necessario quando si
vuole adattare un child a una frazione dello spazio del parent.
return Container(
decoration: BoxDecoration(
border: Border.all(),
),
width: 180,
height: 180,
child: FractionallySizedBox(
alignment: FractionalOffset.center,
widthFactor: 0.5,
heightFactor: 0.5,
child: Container(decoration: BoxDecoration(border:
Border.all()))),
);

Figura 9.14 FractionallySizedBox.

Offstage
Il widget Offstage consente di nascondere all’utente un widget
senza che questo venga effettivamente rimosso dal widget tree.
return Column(
children: [
Offstage(
offstage: _hidden,
child: const Text('Hello World'),
),
ElevatedButton(
onPressed: () {
setState(() {
_hidden = !_hidden;
});
},
child: const Text('Offstage'),
),
],
);

Figura 9.15 Offstage.

Figura 9.16 Widget Text scomparso.

NOTA
Attenzione all’uso del widget Offstage, poiché sarà comunque
presente nell’albero; ciò vuol dire che, anche se non sarà visibile,
consumerà comunque le risorse occupate (per esempio i listener
continueranno a essere attivi).

OverflowBox
Il widget OverflowBox consente di gestire l’overflow di un
figlio. Nel caso in cui avessimo due container annidati, ma il
figlio fosse più grande del genitore, il figlio risulterebbe grande al
massimo quanto il genitore. Per gestire l’eccedenza di spazio
può essere usato il widget in questione.
Center(
child: Container(
decoration: BoxDecoration(border: Border.all()),
alignment: Alignment.center,
width: 250,
height: 250,
child: OverflowBox(
maxHeight: 350,
maxWidth: 350,
child: Container(
width: 350,
height: 350,
decoration: BoxDecoration(border: Border.all()),
child: const Text('Hello World')))));

NOTA
Attenzione alla gestione dell’overflow, poiché si deve tenere conto
degli elementi vicini. In questo caso, se rimuoviamo il widget Center, il
Container figlio va sicuramente in collisione con l’appBar.

Figura 9.17 OverfloxBox.


SizedOverflowBox
Il widget SizedOverflowBox consente di definire la grandezza
di un box che può essere sforato dal figlio. In questo caso
utilizziamolo con il widget Container.
Center(
child: Container(
decoration: BoxDecoration(border: Border.all()),
child: SizedOverflowBox(
size: const Size(50, 50),
child: Container(
height: 20.0,
width: 100.0,
decoration: BoxDecoration(border: Border.all()),
),
)),
);

Figura 9.18 SizedOverfloxBox.

NOTA
I widget OverflowBox e SizedOverfloxBox sono molto simili. La
differenza sta nel fatto che SizedOverfloxBox consente al figlio di
“traboccare” il genitore, mentre OverflowBox impone una grandezza
massima per il traboccamento.

IntrinsicHeight e IntrinsicWidth
I widget IntrinsicHeight e IntrinsicWidth lavorano allo stesso
modo. Entrambi andrebbero usati quando ci troviamo nella
condizione in cui un figlio può espandersi per tutta la grandezza
del genitore e vogliamo limitare questo comportamento alla sua
grandezza intrinseca; ciò sta a significare limitare la dimensione
alla grandezza naturale del figlio. Procediamo con un esempio
chiarificatore.
IntrinsicWidth(
child: Row(children: [
Expanded(
child: Container(
decoration: BoxDecoration(border: Border.all()),
child: const Text('Hello World')))
]));

L’uso di IntrinsicWidth, così come di IntrinsicHeight nel caso


avessimo usato Column invece che Row, consente di ignorare
la conseguenza dell’uso di Expanded. Senza IntrinsicWidth il
child Container avrebbe espanso la riga per tutta la larghezza
del genitore; in questo caso, invece, la riga sarà larga quanto la
sua grandezza intrinseca, ovvero quanto basta per contenere la
stringa “Hello World”.

Multi-child
I widget multi-child sono widget che hanno più figli. Ne
abbiamo già utilizzati diversi e molte volte, ora vediamo di
approfondire l’argomento.

Row
Il widget Row è stato già presentato tra i widget di base
qualche capitolo fa. Vale la pena entrare nel dettaglio del
costruttore e vedere le possibili configurazioni.
Row(
{Key? key,
MainAxisAlignment mainAxisAlignment = MainAxisAlignment.start,
MainAxisSize mainAxisSize = MainAxisSize.max,
CrossAxisAlignment crossAxisAlignment = CrossAxisAlignment.center,
TextDirection? textDirection,
VerticalDirection verticalDirection = VerticalDirection.down,
TextBaseline? textBaseline,
List<Widget> children = const <Widget>[]}
)

Il parametro key vale per tutti i widget e abbiamo già spiegato


come funziona, ma faremo un approfondimento poco più avanti.
mainAxisAligment (allineamento sull’asse principale) consente di

ordinare orizzontalmente i figli. Il valore associabile a


mainAxisAligment è un enum: start, end, center, spaceBetween,

spaceAround e spaceEvenly.
start ed end consentono di allineare i figli il più vicino possibile
all’inizio o alla fine della riga. Un aspetto importante è che per
utilizzare questo valore è necessario che venga definito un
TextDirection. In alcuni esempi precedenti lo abbiamo utilizzato,
in altri no: questo dipende dall’uso di MaterialApp, che di default
offre una configurazione left-to-right, e, se non lo si usa, è
necessario esplicitarlo. Center consente invece di allineare i figli
al centro della riga.
SpaceBetween consente di distribuire lo spazio libero

uniformemente tra i figli, posizionando il primo e l’ultimo figlio


all’estremità. SpaceAround crea lo spazio libero prima del primo
figlio e dopo l’ultimo, che corrisponde alla metà dello spazio
distribuito tra i figli. Infine, spaceEvenly dispone lo spazio libero
uniformemente anche prima del primo figlio e dopo l’ultimo. Fate
riferimento alla Figura 9.19 per vedere le differenze.
return Column(children: [
Container(
margin: const EdgeInsets.all(12),
decoration: BoxDecoration(
border: Border.all(),
),
child:
Row(mainAxisAlignment: MainAxisAlignment.center, children:
const [
Text('1'),
Text('2'),
Text('3'),
])),
Container(
margin: const EdgeInsets.all(12),
decoration: BoxDecoration(
border: Border.all(),
),
child:
Row(mainAxisAlignment: MainAxisAlignment.start, children:
const [
Text('1'),
Text('2'),
Text('3'),
])),
Container(
margin: const EdgeInsets.all(12),
decoration: BoxDecoration(
border: Border.all(),
),
child: Row(mainAxisAlignment: MainAxisAlignment.end, children:
const [
Text('1'),
Text('2'),
Text('3'),
])),
Container(
margin: const EdgeInsets.all(12),
decoration: BoxDecoration(
border: Border.all(),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: const [
Text('1'),
Text('2'),
Text('3'),
])),
Container(
margin: const EdgeInsets.all(12),
decoration: BoxDecoration(
border: Border.all(),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: const [
Text('1'),
Text('2'),
Text('3'),
])),
Container(
margin: const EdgeInsets.all(12),
decoration: BoxDecoration(
border: Border.all(),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: const [
Text('1'),
Text('2'),
Text('3'),
])),
]);

Figura 9.19 MainAxisAlignment.

Il parametro mainAxisSize viene valorizzato con la costante


MainAxisSize, che di default, come possiamo vedere dal
costruttore, ha come parametro max. Questo setting consente di
massimizzare lo spazio a disposizione. Il comportamento
opposto, tramite il valore min, sarà occupare meno spazio
possibile. Entrambi i valori dovranno rispettare eventuali vincoli.
Il parametro CrossAxisAlignment consente di allineare i figli
nell’asse trasversale (orizzontale). I valori che si possono
configurare sono: end, start, center, stretch e baseline. I primi
seguono la logica appena descritta. Stretch, grazie all’uso dei
bordi presente in Figura 9.20, ci mostra che viene usato per
espandere lo spazio destinato ai figli per tutto l’asse. Infine,
baseline consente di allineare i figli seguendo la baseline dei
caratteri; per questo è necessario definire anche textBaseline.
return Column(children: [
Container(
margin: const EdgeInsets.all(12),
height: 50,
decoration: BoxDecoration(
border: Border.all(),
),
child: Row(crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
decoration: BoxDecoration(
border: Border.all(),
),
child: const Text('1')),
const Text('2'),
const Text('3'),
])),
Container(
margin: const EdgeInsets.all(12),
height: 50,
decoration: BoxDecoration(
border: Border.all(),
),
child: Row(crossAxisAlignment: CrossAxisAlignment.end,
children: [
Container(
decoration: BoxDecoration(
border: Border.all(),
),
child: const Text('1')),
const Text('2'),
const Text('3'),
])),
Container(
margin: const EdgeInsets.all(12),
height: 50,
decoration: BoxDecoration(
border: Border.all(),
),
child: Row(crossAxisAlignment: CrossAxisAlignment.center,
children: [
Container(
decoration: BoxDecoration(
border: Border.all(),
),
child: const Text('1')),
const Text('2'),
const Text('3'),
])),
Container(
margin: const EdgeInsets.all(12),
height: 50,
decoration: BoxDecoration(
border: Border.all(),
),
child: Row(crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Container(
decoration: BoxDecoration(
border: Border.all(),
),
child: const Text('1')),
const Text('2'),
const Text('3'),
])),
]);

Figura 9.20 CrossAxisAligment.

La proprietà TextDirection è stata già introdotta, ma ripetiamo


ugualmente che è necessaria nel momento in cui ci troviamo
fuori l’uso di una MaterialApp e, in questo caso, andrebbe
definita che tipologia di direzione del testo vogliamo adottare.
Pensiamo per esempio all’ebraico, che si legge da destra verso
sinistra.
La proprietà VerticalDirection, di default top, dispone i figli
dall’alto verso il basso lungo l’asse verticale. Per invertire
questa logica si può usare il valore down.
La proprietà TextBaseline può essere usata per indicare quale
tipologia di testo vogliamo usare all’interno della riga, se di tipo
alfabetico o ideogramma.
Infine, fulcro del widget Row è il parametro children, che
consente di definire una lista di figli.

Column
Esattamente come Row esiste per la gestione delle righe,
Column esiste per la gestione delle colonne. Vediamo il suo
costruttore.
Column(

{Key? key,
MainAxisAlignment mainAxisAlignment = MainAxisAlignment.start,
MainAxisSize mainAxisSize = MainAxisSize.max,
CrossAxisAlignment crossAxisAlignment = CrossAxisAlignment.center,
TextDirection? textDirection,
VerticalDirection verticalDirection = VerticalDirection.down,
TextBaseline? textBaseline,
List<Widget> children = const <Widget>[]}

Le logiche dei parametri seguono quanto già visto con il


widget Row, con la differenza che chiaramente il main e il cross
axis sono invertiti. Vi invito a realizzare l’esempio ritratto in
Figura 9.21.
Figura 9.21 Column.

Flex
Flex è un widget che consente di utilizzare Column o Row
attraverso l’uso di uno specifico parametro: direction. Potete
utilizzarlo come fosse Row, con la property direction settata con
Axis.horizontal, o con Axis.vertical per Column.
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Example'),
),
body: Container(
margin: const EdgeInsets.all(12),
decoration: BoxDecoration(
border: Border.all(),
),
child: Flex(
direction: Axis.vertical,
mainAxisAlignment: MainAxisAlignment.start,
children: const [
Text('1'),
Text('2'),
Text('3'),
])),
);
}

Stack
Abbiamo già introdotto il widget Stack. Esso consente una
sovrapposizione di widget. Vediamo nel dettaglio il costruttore.
Stack(
{Key? key,
AlignmentGeometry alignment = AlignmentDirectional.topStart,
TextDirection? textDirection,
StackFit fit = StackFit.loose,
Clip clipBehavior = Clip.hardEdge,
Listlt;Widget> children = const lt;Widget>[]}
)

Rispetto agli altri parametri, di cui ormai conosciamo il


significato e i possibili usi, il widget Stack ha un parametro
interessante: fit. Il valore di default è loose e impone una
corretta gestione dei limiti. Un altro possibile valore è expand, che
forza i figli non posizionati a espandersi fino al limite dei vincoli
di larghezza e altezza. Infine, il valore passthrough consente ai figli
non posizionati di ereditare eventuali limiti che avvolgono Stack.
NOTA
Per figli non posizionati si fa riferimento ai widget che non vengono
avvolti dal widget Positioned.

Vi invito a giocare con l’esempio proposto per comprendere a


fondo i valori associabili a fit.
return ConstrainedBox(
constraints: const BoxConstraints(minHeight: 300, minWidth: 300),
child: Stack(
fit: StackFit.passthrough,
children: <Widget>[
Container(
width: 400,
height: 100,
decoration: BoxDecoration(
border: Border.all(),
),
),
Container(
width: 30,
height: 30,
decoration: BoxDecoration(
border: Border.all(),
),
),
Container(
height: 60,
width: 120,
decoration: BoxDecoration(
border: Border.all(),
),
),

IndexedStack
Il widget IndexedStack è una pila, che mostra esclusivamente
un figlio tramite l’uso di un indice.
Listato 9.2 main.dart
import 'dart:math';
import 'package:flutter/material.dart';

void main() {
runApp(const MyApp());
}

class MyApp extends StatelessWidget {


const MyApp({super.key});

@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
home: Scaffold(
appBar: AppBar(
title: const Text('Example'),
),
body: const IndexedStackPage()));
}
}

class IndexedStackPage extends StatefulWidget {


const IndexedStackPage({super.key});

@override
State<IndexedStackPage> createState() => _IndexedStackPageState();
}
class _IndexedStackPageState extends State<IndexedStackPage> {
int index = 0;

@override
Widget build(BuildContext context) {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
IndexedStack(
index: index,
children: [
Container(
decoration: BoxDecoration(border: Border.all()),
alignment: Alignment.center,
child: const Text(
'0',
style: TextStyle(fontSize: 100),
),
),
Container(
decoration: BoxDecoration(border: Border.all()),
alignment: Alignment.center,
child: const Text('1', style: TextStyle(fontSize: 100)),
),
Container(
decoration: BoxDecoration(border: Border.all()),
alignment: Alignment.center,
child: const Text('2', style: TextStyle(fontSize: 100)),
)
],
),
ElevatedButton(
onPressed: () {
setState(() {
index = Random().nextInt(3);
});
},
child: const Text('Change'),
)
],
);
}
}

L’indice index, parametro del widget IndexedStack, consente di


visualizzare uno dei figli del widget stesso. L’indice corrisponde
alla posizione del figlio nella lista. Nel nostro caso, l’indice “0”
corrisponderà al primo figlio della lista. L’uso della classe
Random e il suo metodo nextInt consente di valorizzare index
con un intero tra 0 e 2.

Figura 9.22 IndexedStack.


Figura 9.23 IndexedStack(2).

Table
Il widget Table consente la creazione di una tabella. La tabella
è composta da una lista di figli, che corrispondono alle righe;
questa, a sua volta, conterrà una lista di celle. Facciamo un
esempio.
return Padding(
padding: const EdgeInsets.all(16.0),
child: Table(
children: const <TableRow>[
TableRow(
children: <Widget>[
TableCell(child: Text('1')),
TableCell(child: Text('Luca')),
TableCell(child: Text('Roma')),
],
),
TableRow(
children: <Widget>[
TableCell(child: Text('2')),
TableCell(child: Text('Simone')),
TableCell(child: Text('Milano')),
],
),
TableRow(
children: <Widget>[
TableCell(child: Text('3')),
TableCell(child: Text('Alessandro')),
TableCell(child: Text('Catania')),
],
),
],
));

Il widget Table si compone del widget TableRow, che definisce


una lista di figli che saranno ordinati orizzontalmente. È
importante che ogni TableRow ospiti lo stesso numero di figli.
Stiamo utilizzando TableCell, che offre funzionalità di
ordinamento; in questo specifico caso, tuttavia, ne avremmo
potuto tranquillamente fare a meno e avremmo potuto usare
direttamente i widget Text.

Figura 9.24 Table.

È possibile personalizzare alcuni aspetti della tabella grazie ai


parametri del suo costruttore. Uno su tutti è l’uso di columnWidths,
che consente di definire la grandezza di specifiche celle, per
esempio:
columnWidths: const {
0: FixedColumnWidth(50.0),
},

In questo caso fissiamo a 50px la larghezza del primo figlio di


ogni TableRow.
Figura 9.25 columnWidths.

Wrap
Il widget Wrap consente di allineare i figli orizzontalmente o
verticalmente senza preoccuparsi necessariamente delle
problematiche di overflow. I figli verranno posizionati sempre
adiacenti agli altri figli del widget Wrap; se viene meno lo spazio,
il child verrà posizionato su una nuova riga o colonna.
return Wrap(direction: Axis.horizontal, children: <Widget>[
Container(
width: 250,
height: 100,
decoration: BoxDecoration(border: Border.all()),
),
Container(
width: 100,
height: 100,
decoration: BoxDecoration(border: Border.all()),
),
Container(
width: 250,
height: 100,
decoration: BoxDecoration(border: Border.all()),
),
]);

Figura 9.26 Wrap (horizontal).

return Wrap(direction: Axis.vertical, children: <Widget>[


Container(
width: 150,
height: 600,
decoration: BoxDecoration(border: Border.all()),
),
Container(
width: 200,
height: 500,
decoration: BoxDecoration(border: Border.all()),
),
Container(
width: 250,
height: 200,
decoration: BoxDecoration(border: Border.all()),
),
]);
Figura 9.27 Wrap (vertical).

CustomMultiChildLayout
Il widget CustomMultiChildLayout, a differenza del widget
CustomSingleChildLayout, delega la creazione di una lista di
figli. Prendiamo spunto dall’esempio precedente e procediamo
con un nuovo esempio.
Listato 9.3 main.dart
import 'package:flutter/material.dart';

void main() {
runApp(const MyApp());
}

class MyApp extends StatelessWidget {


const MyApp({super.key});

@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
home: Scaffold(
appBar: AppBar(
title: const Text('Example'),
),
body: const ExamplePage()));
}
}

class ExamplePage extends StatelessWidget {


const ExamplePage({super.key});

@override
Widget build(BuildContext context) {
return Center(
child: Container(
decoration: BoxDecoration(border: Border.all()),
height: 150,
width: 150,
child: CustomMultiChildLayout(
delegate: _MyMultiChildLayoutDelegate(),
children: [
LayoutId(
id: 1,
child: Container(
decoration: BoxDecoration(border: Border.all()),
child: const Center(child: Text('1')),
),
),
LayoutId(
id: 2,
child: Container(
decoration: BoxDecoration(border: Border.all()),
child: const Center(child: Text('2')),
)),
],
)));
}
}
class _MyMultiChildLayoutDelegate extends MultiChildLayoutDelegate {
_MyMultiChildLayoutDelegate();

@override
void performLayout(Size size) {
layoutChild(
1, BoxConstraints(maxWidth: size.width, maxHeight: size.height /
2));
positionChild(1, const Offset(0, 0));

layoutChild(
2, BoxConstraints(maxWidth: size.width, maxHeight: size.height /
2));

positionChild(2, Offset(0, size.height / 2));


}

@override
bool shouldRelayout(_MyMultiChildLayoutDelegate oldDelegate) {
return false;
}
}

Il delegato dovrà estendere la classe


MultiChildLayoutDelegate. Quest’ultima dovrà sovrascrivere i
due metodi: performLayout e shouldRelayout. performLayout consente
di disporre ogni figlio; questo può essere fatto tramite i metodi
layoutChild e positionChild.

Il metodo shouldRelayout, invece, viene invocato se il delegato


cambia.
I metodi layoutChild e positionChild consentono di imporre
vincoli e configurare la posizione. Questi metodi fanno uso di un
id per identificare il figlio. Il suddetto id è quello che viene
definito tramite la classe layoutId nel widget
CustomMultiChildLayout.
Figura 9.28 CustomMultiChildLayout.

LayoutBuilder
Il widget LayoutBuilder consente di creare un albero di widget
che può interagire con i vincoli. Il widget in questione verrà
aggiornato quando i vincoli del genitore cambieranno.

Listato 9.4 main.dart


import 'package:flutter/material.dart';

void main() {
runApp(const MyApp());
}

class MyApp extends StatelessWidget {


const MyApp({super.key});

@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
home: Scaffold(
appBar: AppBar(
title: const Text('Example'),
),
body: Center(child: LayoutBuilder(
builder: (BuildContext context, BoxConstraints constraints)
{
return customRow(
maxHeight: constraints.maxHeight,
maxWidth: constraints.maxWidth);
})),
));
}
}

Widget customRow({maxHeight, maxWidth}) {


return Row(mainAxisAlignment: MainAxisAlignment.center, children: [
Container(
decoration: BoxDecoration(border: Border.all()),
height: maxHeight / 4,
width: maxWidth / 2,
child: const Text('1', style: TextStyle(fontSize: 100)),
),
Container(
decoration: BoxDecoration(border: Border.all()),
height: maxHeight / 4,
width: maxWidth / 2,
child: const Text('2', style: TextStyle(fontSize: 100)),
)
]);
}

Figura 9.29 LayoutBuilder.

NOTA
A differenza del widget Builder, il widget LayoutBuilder viene eseguito
in fase di rendering; infatti, abbiamo lì la possibilità di disporre dei
vincoli del genitore.

BoxConstraints
La classe BoxConstraints definisce dei vincoli legati al sizing
che il parent impone al proprio figlio. Il costruttore di
BoxConstraints è così definito:
const BoxConstraints(
{double minWidth = 0.0,
double maxWidth = double.infinity,
double minHeight = 0.0,
double maxHeight = double.infinity}
)

I parametri minWidth e maxWidth, piuttosto che minHeight e


maxHeight, definiscono quanto effettivamente possono essere
larghi e alti gli elementi child. Nel caso in cui il figlio sforià un
vincolo, Flutter segnalerà un overflow.

Flow
Il widget Flow consente di creare e posizionare dei figli tramite
l’uso di uno speciale delegato. Il widget Flow è usato spesso per
la creazione di menu dinamici. Nell’esempio proposto creiamo
un menu a cascata.

Listato 9.5 main.dart


import 'package:flutter/material.dart';

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

class MyApp extends StatelessWidget {


const MyApp({super.key});

@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(
title: const Text('Example'),
),
body: const FlowMenu(),
),
);
}
}

class FlowMenu extends StatefulWidget {


const FlowMenu({super.key});

@override
State<FlowMenu> createState() => _FlowMenuState();
}

class _FlowMenuState extends State<FlowMenu>


with SingleTickerProviderStateMixin {
late AnimationController menuAnimation;
IconData selectedIcon = Icons.home;
final List<IconData> menuItems = <IconData>[
Icons.home,
Icons.settings,
Icons.menu,
];

void _updateMenu(IconData icon) {


if (icon != Icons.menu) {
setState(() => selectedIcon = icon);
}
}

@override
void initState() {
super.initState();
menuAnimation = AnimationController(
duration: const Duration(milliseconds: 150),
vsync: this,
);
}

Widget flowMenuItem(IconData icon) {


return ElevatedButton(
onPressed: () {
_updateMenu(icon);
menuAnimation.status == AnimationStatus.completed
? menuAnimation.reverse()
: menuAnimation.forward();
},
style: ElevatedButton.styleFrom(
shape: const CircleBorder(),
padding: const EdgeInsets.all(18),
backgroundColor: icon == selectedIcon ? Colors.grey :
Colors.blue,
),
child: Icon(icon, color: Colors.white, size: 18),
);
}

@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(8),
child: Flow(
delegate: FlowMenuDelegate(menuAnimation: menuAnimation),
children: menuItems
.map<Widget>((IconData icon) => flowMenuItem(icon))
.toList(),
));
}
}

class FlowMenuDelegate extends FlowDelegate {


FlowMenuDelegate({required this.menuAnimation})
: super(repaint: menuAnimation);

final Animation<double> menuAnimation;

@override
bool shouldRepaint(FlowMenuDelegate oldDelegate) {
return menuAnimation != oldDelegate.menuAnimation;
}

@override
void paintChildren(FlowPaintingContext context) {
double y = 0.0;
for (int i = 0; i < context.childCount; ++i) {
y = context.getChildSize(i)!.width * i;
context.paintChild(
i,
transform: Matrix4.translationValues(
0,
y * menuAnimation.value,
0,
),
);
}
}
}

Il codice è caratterizzato dall’uso del widget Flow, che


definisce un delegato e una lista di figli. Il delegato
FlowMenuDelegate deve estendere la classe FlowDelegate.
Quest’ultima può implementare diversi metodi, tra i quali
shouldRepaint e paintChildren, ma ne implementa anche altri di cui

non facciamo uso. Gli altri metodi sono: getConstraintsForChild,


getSize e shouldRelayout.
L’esempio in oggetto fa uso di un’animazione e il delegato
FlowMenuDelegate usa il controller per gestire l’animazione. Quando

l’utente interagisce con il bottone, oltre che in fase di build,


Flutter invoca il metodo paintChildren per posizionare il widget
lungo l’asse y, tramite il metodo translationValues. Al successivo
tap sul menu, i figli vengono riposizionati come in origine.
L’animazione è possibile grazie all’uso del valore
menuAnimation.value. Il valore dato in pasto a

Matrix4.translationValues corrisponderà a un valore crescente da 0


a 1, che per diversi valori intermedi verrà moltiplicato per il width
dell’icona. Infine, il risultato di questa moltiplicazione aritmetica
verrà proiettato sull’asse y, consentendo il movimento e di
conseguenza l’animazione, posizionando di fatto l’icona in
posizione da 0 al “width” durante l’elapsed dell’animazione. Nel
processo inverso, menuAnimation.value conterrà i valori intermedi
da 1 a 0, e l’animazione tornerà allo stato iniziale. Il metodo
paintChildren verrà chiamato parecchie volte, creando di fatto

un’animazione fluida.
Il metodo shouldRepaint indica a Flutter di ridisegnare i figli nel
caso in cui l’istanza del controllore sia cambiata durante
l’esecuzione dell’applicativo. Nel nostro caso questo metodo
ritornerà sempre false,e non influenzerà il comportamento.
Il metodi getConstraintsForChild, getSize e shouldRelayout sono utili
rispettivamente a catturare i vincoli del genitore, indicare la
grandezza del container che ospiterà i figli (Flow) e infine
ridisporre i figli a fronte di un cambio di layout.
Flow è un widget che fa uso di un’animazione, ed è giunto il
momento di affrontare nel dettaglio questo tema molto
interessante. Il prossimo capitolo ha come argomento proprio le
animazioni in Flutter.

Figura 9.30 Menu (Flow).


Figura 9.31 Menu a cascata (Flow).
Capitolo 10

Animazioni

Flutter offre diversi strumenti che consentono la creazione di


animazioni più o meno complesse. Le animazioni sono alla base
di qualsiasi design moderno ed è fondamentale che siano fluide
per rendere la user experience più accattivante e migliorare
l’esperienza d’uso generale della nostra applicazione. Rendere
dinamici, e di conseguenza interattivi, alcuni elementi della UI è
un’attività che a oggi può essere definita come indispensabile.
Alcune animazioni le abbiamo già affrontate lungo il percorso,
ma in questo capitolo approfondiremo il tema, provando a capire
come si comporta il framework.

Controller
AnimationController è il cuore delle animazioni in Flutter. Il
controller deve essere gestito all’interno dello state del nostro
widget, e verrà utilizzato per controllare l’animazione.
Prendiamo per esempio questa porzione di sorgente.
class _AnimationPageState extends State<AnimationPage>
with SingleTickerProviderStateMixin {
late AnimationController controller;

@override
void initState() {
super.initState();
controller =
AnimationController(duration: const Duration(seconds: 2), vsync:
this);
}
Il primo passo è quello di comprendere l’uso del mixin
SingleTickerProviderStateMixin. La suddetta classe è necessaria

per l’utilizzo del parametro vsync del costruttore


AnimationController. Vsync, se guardiamo la definizione del
costruttore, è di tipo TickerProvider; in altre parole è un ticker.

Ticker
Il ticker in Flutter è uno strumento che ci consente di
comunicare con il framework nelle fasi in cui viene renderizzato
un nuovo frame. Flutter renderizza i suoi frame alla velocità di
60fps (sui device che lo supportano anche a 120fps). Quando
qualcosa nel layout della nostra app cambia è necessario
generare un nuovo frame. Il cambiamento nella nostra
interfaccia può essere di qualsiasi tipo; nel nostro caso
immaginiamo che la causa scatenante sia una banale
animazione.
NOTA
fps è l’acronimo di frames per second, in italiano fotogrammi per
secondo. Questa unità di misura indica quanti fotogrammi il nostro
device può renderizzare in un solo secondo: più alto sarà questo
valore più fluida sarà l’animazione.

Il ticker consente di essere notificati da parte del framework


nel momento in cui è necessario renderizzare un nuovo frame.
class _AnimationPageState extends State<AnimationPage>
with SingleTickerProviderStateMixin {
late final Ticker _ticker;

@override
void initState() {
super.initState();
_ticker = createTicker((elapsed) {
debugPrint('elapsed ${elapsed.inSeconds}');
});
_ticker.start();
}

In questo esempio abbiamo creato un ticker. Il metodo


creatTicker consente di definire una callback che verrà richiamata

per ogni fps. È necessario avviare il ticker tramite il metodo start


affinché la callback venga eseguita. L’argomento elapsed
scandisce il tempo tra un ciclo e l’altro. In Figura 10.1 possiamo
vedere quanto volte viene eseguita la callback per lo stesso
secondo elapsed.inSeconds. Possiamo evincere che il rendering
viene effettuato a 60fps, un frame circa ogni 16ms.

Figura 10.1 Log ticker.

I ticker, nel caso ne istanziassimo più di uno, risulterebbero


sincronizzati. Questo perché lo scheduler del framework, colui
che effettivamente si preoccupa di eseguire la callback del
ticker, invia l’evento per la gestione del nuovo frame a tutti i
ticker in ascolto allo stesso momento. Questo consente di avere
una gestione del ciclo di rendering uguale per tutti i widget.
Una volta chiarito che cosa sia un ticker, è necessario l’uso
nell’AnimationController in quanto verrà utilizzato un ticker
interno (vsync) semplicemente per sincronizzarsi con il
processo di rendering.
Un esempio di animazione, molto basica, può essere creata
grazie al solo ausilio dell’AnimationController. Tenendo a mente
quanto visto precedentemente, l’AnimationController è stato
istanziato indicando il ticker e una durata.
Durante la sua esecuzione, l’AnimationController ritorna dei
valori che hanno una progressione da 0 a 1. Immaginiamo di
voler far scomparire un widget: in questo caso i valori prodotti
dal range tra 0 e 1 saranno alla base della nostra animazione.
@override
Widget build(BuildContext context) {
return Center(
child: AnimatedBuilder(
animation: _controller,
builder: (BuildContext _, child) {
debugPrint('AnimationController: ${_controller.value}');
return Opacity(
opacity: _controller.value,
child: const Text("Hello World"),
);
},
));
}

Grazie all’uso del widget AnimatedBuilder colleghiamo il


nostro widget al controller precedentemente creato nello state.
La funzione di building verrà invocata a ogni ciclo di rendering,
come abbiamo appena visto introducendo il ticker. A ogni ciclo il
valore di _controller.value sarà diverso, incrementale rispetto al
range 0-1, consentendo di creare l’animazione. Di fatto, il
parametro opacity del widget Opacity conterrà tutti i valori del
range 0-1, e ciò è perfettamente in linea con quanto si aspetta il
parametro opacity del widget Opacity.
L’opacità del widget Opacity, guidata dalla property opacity e
dal valore _controller.value, aumenterà per tutta la durata
dell’animazione rispetto ai valori generati dal controller.
"flutter: AnimationController: 0.6066153333333331
flutter: AnimationController: 0.6093929999999999
flutter: AnimationController: 0.6121708333333338
flutter: AnimationController: 0.6149491666666664
flutter: AnimationController: 0.6177263333333336
flutter: AnimationController: 0.6205041666666666
flutter: AnimationController: 0.6232819999999997"

Potete usare il metodo repeat per creare un loop.


_controller.repeat(reverse: true);

Questa tipologia di animazione si basa sullo stesso principio


di quanto presentato nel widget Flow.
Creiamo un esempio completo.

Listato 10.1 main.dart


import 'package:flutter/material.dart';

void main() {
runApp(const MyApp());
}

class MyApp extends StatelessWidget {


const MyApp({super.key});

@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
home: Scaffold(
appBar: AppBar(
title: const Text('Example'),
),
body: const AnimationPage()));
}
}

class AnimationPage extends StatefulWidget {


const AnimationPage({super.key});

@override
State<AnimationPage> createState() => _AnimationPageState();
}

class _AnimationPageState extends State<AnimationPage>


with SingleTickerProviderStateMixin {
late AnimationController _controller;
@override
void initState() {
super.initState();
_controller = AnimationController(
duration: const Duration(seconds: 2),
vsync: this,
)
..forward()
..addListener(() {
if (_controller.isCompleted) {
_controller.repeat(reverse: true);
}
});
}

@override
void dispose() {
_controller.dispose();
super.dispose();
}

@override
Widget build(BuildContext context) {
return Center(
child: AnimatedBuilder(
animation: _controller,
builder: (BuildContext _, child) {
debugPrint('AnimationController: ${_controller.value}');
return Opacity(
opacity: _controller.value,
child: const Text("Hello World"),
);
},
));
}
}

Il widget AnimatedOpacity implementa già la logica


dell’esempio appena proposto. Per questo caso di studio
optiamo per riprodurre il widget AnimatedOpacity con un
approccio “manuale”, utilizzando AnimationController per
simulare il suo comportamento.
Il controller AnimationController viene definito dentro initState
e, come proposto, viene “liberato” nel metodo dispose (anche se
in questo esempio non era necessario). Il metodo forward
dell’AnimationController consente di avviare l’animazione e
tramite il metodo addListener possiamo metterci in ascolto della
property isCompleted, che ci consente di intercettare la
conclusione dell’animazione e ripeterla tramite il metodo repeat.
La property reverse, esplicitata nel metodo repeat, consente di
ripetere l’animazione, non partendo da 0, ma a ritroso rispetto
alla sua conclusione (da 1 a 0).
NOTA
In questo sorgente abbiamo fatto uso dell’operatore cascade.

L’operatore in questione consente di accedere in sequenza ai metodi


di un’istanza di una classe direttamente tramite l’operatore ‘..’ senza
doversi appoggiare a una variabile.

Tipologie
Le animazioni a cui possiamo attingere in Flutter sono
essenzialmente di due tipi. Il primo è basato sulla simulazione
della fisica e il secondo è di tipo lineare.
Un esempio banale di animazione basata sulla fisica potrebbe
essere quella che simula l’effetto della gravità o di oscillazione.
L’uso di queste animazioni può rendere davvero accattivanti le
applicazioni. Questa tipologia di animazione non richiede di
essere maghi della fisica o di generare calcoli complessi, poiché
Flutter offre una serie di strumenti per semplificare lo sviluppo di
queste animazioni. Un esempio piuttosto confortante al
momento può essere dato dall’uso della classe
GravitySimulation. Quest’ultima, per esempio, può generare
un’animazione che ha un’accelerazione di 100 pixel per
secondo, che parte dal pixel 0 fino al pixel 500 con una velocità
iniziale di 0 pixel per secondo.
GravitySimulation(
100.0, // accelerazione
0.0, // inizio
500.0, // fine
0.0, // velocità iniziale
);
Physics
Procediamo con un esempio che fa uso della fisica.

Listato 10.2 main.dart


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

void main() {
runApp(const MyApp());
}

class MyApp extends StatelessWidget {


const MyApp({super.key});

@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
home: Scaffold(
appBar: AppBar(
title: const Text('Example'),
),
body: const AnimationExample()));
}
}

class AnimationExample extends StatefulWidget {


const AnimationExample({super.key});

@override
State<AnimationExample> createState() => _AnimationExampleState();
}

class _AnimationExampleState extends State<AnimationExample>


with SingleTickerProviderStateMixin {
late AnimationController controller;
late GravitySimulation simulation;

@override
void initState() {
super.initState();

simulation = GravitySimulation(
100.0, // acceleration
0.0, // starting point
500.0, // end point
0.0, // starting velocity
);

controller = AnimationController(upperBound: 500, vsync: this)


..addListener(() {
setState(() {});
});

controller.animateWith(simulation);
}

@override
Widget build(BuildContext context) {
return Stack(children: [
Positioned(
left: 50,
top: controller.value,
height: 50,
width: 50,
child: Container(
decoration: BoxDecoration(border: Border.all()),
),
),
]);
}
}

In questo esempio abbiamo fatto uso della classe


GravitySimulation, definita come “physics library”. Le altre
librerie fornite da Flutter possono essere consultate a questo
indirizzo: https://api.flutter.dev/flutter/physics/physics-library.html.
L’AnimationController fa uso della property upperBound che
interpola i valori da 0 a 500. Il valore 500 corrisponde anche a
quanto configurato dall’oggetto simulation. Infine, l’animazione
viene guidata dalla simulazione tramite l’uso del metodo
animateWith. In questo esempio abbiamo fatto uso di un’altra

tecnica per renderizzare i frame, ovvero ..addListener con setState


piuttosto che AnimatedBuilder. È consigliato, comunque, l’uso di
AnimatedBuilder, più efficiente in fase di rendering poiché esegue
esclusivamente il rebuild del suo builder.
Figura 10.2 Animazione (start ed end).

Tween
Le animazioni definibili come Tween hanno un approccio
lineare. Non fanno altro che tradurre i valori 0.0-1.0
dell’AnimationController in un range più ampio. Hanno un
inizio e una fine definiti per una durata precisa. Inoltre è
anche possibile introdurre una curva affinchè l’animazione
non sia “del tutto” lineare.
class AnimationExample extends StatefulWidget {
const AnimationExample({super.key});

@override
State<AnimationExample> createState() => _AnimationExampleState();
}

class _AnimationExampleState extends State<AnimationExample>


with SingleTickerProviderStateMixin {
late AnimationController controller;
late Animation<double> animation;

@override
void initState() {
super.initState();

controller =
AnimationController(vsync: this, duration: const
Duration(seconds: 3));
animation = Tween<double>(begin: 0, end: 150).animate(controller);
animation.addListener(() {
setState(() {});
});
animation.addStatusListener((status) {
if (status == AnimationStatus.completed) {
controller.reverse();
} else if (status == AnimationStatus.dismissed) {
controller.forward();
}
});
controller.forward();
}

@override
Widget build(BuildContext context) {
return Stack(children: [
Positioned(
left: animation.value,
top: 10,
height: 50,
width: 50,
child: Container(
decoration: BoxDecoration(border: Border.all()),
),
),
]);
}
}
Questo esempio, che si basa su un semplice movimento, è
realizzato grazie a un remapping dei valori generati
dall’AnimationController. Il range 0-1, a cui abbiamo fatto
riferimento qualche esempio fa, viene rimappato con un range
0-150. L’animazione, eseguita dal metodo animate di Tween,
genererà a ogni rendering un valore (animation.value) che verrà
espresso con valori intermedi più distanti l’uno dall’altro
rispetto al range 0-1, ma scandito dallo stesso numero di fps.
Figura 10.3 Tween (start ed end).

Curve
Abbiamo accennato al concetto di curva. L’uso della
curva, in Flutter, ci consente di controllare la velocità
dell’animazione in una porzione specifica di tempo, ovvero
in una fase dell’esecuzione. In Figura 10.4 viene mostrato
l’uso della curva easeInQuint e come questa esegua
un’accelerazione esponenziale nella parte finale
dell’animazione.
È possibile usare le curve tramite la classe
CurvedAnimation. Quest’ultima userà Interval per definire
l’intervallo dell’animazione rispetto alla sua durata e la
tipologia di animazione. Flutter offre tante implementazioni
di curve; è possibile reperire la lista qui:
https://api.flutter.dev/flutter/animation/Curves-class.html.

Figura 10.4 Curve - animazione easeInQuint.

class _AnimationExampleState extends State<AnimationExample>


with SingleTickerProviderStateMixin {
late AnimationController _controller;
late Animation<double> animation;

@override
void initState() {
super.initState();

_controller =
AnimationController(vsync: this, duration: const
Duration(seconds: 6));
_controller.repeat(reverse: true);

animation = Tween(
begin: 0.0,
end: 500.0,
).animate(
CurvedAnimation(
parent: _controller,
curve: const Interval(
0.0,
1.0,
curve: Curves.easeInQuint,
),
),
);
}

@override
Widget build(BuildContext context) {
return Center(
child: AnimatedBuilder(
animation: _controller,
builder: (BuildContext _, child) {
return Stack(children: [
Positioned(
left: 0,
top: animation.value,
height: 50,
width: 50,
child: Container(
decoration: BoxDecoration(border: Border.all()),
),
),
]);
},
));
}
}

Come già visto nell’esempio precedente, in questo caso


al metodo animate di Tween colleghiamo CurvedAnimation, che
a sua volta utilizza il controller per gestire l’animazione. La
classe Interval consente infine la presenza dell’animazione
Curves.easeInQuint nell’intervallo definito (in questo esempio
di tutta la durata dell’animazione).
NOTA
È possibile creare curve personalizzate estendendo la classe
Curve e facendo l’override del metodo transformInternal.
Quest’ultimo torna il valore di “x” (accelerazione) in base a “t”
(tempo).
Capitolo 11

Gesture

Le gesture sono uno strumento piuttosto comune in tutte le


applicazioni touch-based. Flutter offre tutti gli strumenti
necessari per individuare le gesture utilizzate dall’utente e per
crearne di nuove.

GestureDetector
Il widget GestureDetector consente di rilevare le gesture
utilizzate dell’utente. Il suo funzionamento si basa sull’avvolgere
il widget che interagirà con le gesture.
Il costrutto del widget in oggetto è costituito da un’infinità di
callback associati a diverse tipologie di gesture.
GestureDetector(
{Key? key,
Widget? child,
GestureTapDownCallback? onTapDown,
GestureTapUpCallback? onTapUp,
GestureTapCallback? onTap,
GestureTapCancelCallback? onTapCancel,
GestureTapCallback? onSecondaryTap,
GestureTapDownCallback? onSecondaryTapDown,
GestureTapUpCallback? onSecondaryTapUp,
GestureTapCancelCallback? onSecondaryTapCancel,
GestureTapDownCallback? onTertiaryTapDown,
GestureTapUpCallback? onTertiaryTapUp,
GestureTapCancelCallback? onTertiaryTapCancel,
GestureTapDownCallback? onDoubleTapDown,
GestureTapCallback? onDoubleTap,
GestureTapCancelCallback? onDoubleTapCancel,
GestureLongPressDownCallback? onLongPressDown,
GestureLongPressCancelCallback? onLongPressCancel,
GestureLongPressCallback? onLongPress,
GestureLongPressStartCallback? onLongPressStart,
GestureLongPressMoveUpdateCallback? onLongPressMoveUpdate,
GestureLongPressUpCallback? onLongPressUp,
GestureLongPressEndCallback? onLongPressEnd,
GestureLongPressDownCallback? onSecondaryLongPressDown,
GestureLongPressCancelCallback? onSecondaryLongPressCancel,
GestureLongPressCallback? onSecondaryLongPress,
GestureLongPressStartCallback? onSecondaryLongPressStart,
GestureLongPressMoveUpdateCallback? onSecondaryLongPressMoveUpdate,
GestureLongPressUpCallback? onSecondaryLongPressUp,
GestureLongPressEndCallback? onSecondaryLongPressEnd,
GestureLongPressDownCallback? onTertiaryLongPressDown,
GestureLongPressCancelCallback? onTertiaryLongPressCancel,
GestureLongPressCallback? onTertiaryLongPress,
GestureLongPressStartCallback? onTertiaryLongPressStart,
GestureLongPressMoveUpdateCallback? onTertiaryLongPressMoveUpdate,
GestureLongPressUpCallback? onTertiaryLongPressUp,
GestureLongPressEndCallback? onTertiaryLongPressEnd,
GestureDragDownCallback? onVerticalDragDown,
GestureDragStartCallback? onVerticalDragStart,
GestureDragUpdateCallback? onVerticalDragUpdate,
GestureDragEndCallback? onVerticalDragEnd,
GestureDragCancelCallback? onVerticalDragCancel,
GestureDragDownCallback? onHorizontalDragDown,
GestureDragStartCallback? onHorizontalDragStart,
GestureDragUpdateCallback? onHorizontalDragUpdate,
GestureDragEndCallback? onHorizontalDragEnd,
GestureDragCancelCallback? onHorizontalDragCancel,
GestureForcePressStartCallback? onForcePressStart,
GestureForcePressPeakCallback? onForcePressPeak,
GestureForcePressUpdateCallback? onForcePressUpdate,
GestureForcePressEndCallback? onForcePressEnd,
GestureDragDownCallback? onPanDown,
GestureDragStartCallback? onPanStart,
GestureDragUpdateCallback? onPanUpdate,
GestureDragEndCallback? onPanEnd,
GestureDragCancelCallback? onPanCancel,
GestureScaleStartCallback? onScaleStart,
GestureScaleUpdateCallback? onScaleUpdate,
GestureScaleEndCallback? onScaleEnd,
HitTestBehavior? behavior,
bool excludeFromSemantics = false,
DragStartBehavior dragStartBehavior = DragStartBehavior.start,
Set<PointerDeviceKind>? supportedDevices}
)

Il costruttore ci dà la prova di quante gesture siano rilevabili.


Prendiamo in esempio le più comuni:
onTapDown: tocco dello schermo;
onTapUp: rilascio del tocco;
onTap: onTapDown + onTapUp;
onDoubleTap: onDoubleTapDown + onDoubleTapUp,
doppio tap (2 dita);
onLongPress: evento di contatto prolungato dello schermo;
onPanStart/onPanUpdate/onPanEnd: eventi di
trascinamento;
onHorizontalDragStart/onHorizontalDragUpdate: eventi di
trascinamento con dettaglio orizzontale;
onVerticalDragStart/onVerticalDragUpdate: eventi di
trascinamento con dettaglio verticale;
onScaleStart/onScaleUpdate/onScaleEnd: eventi legati alla
tipica gesture di “zoom”, che consente di aumentare e
diminuire lo scale.
Inoltre, è anche possibile accedere a molti elementi di
dettaglio riguardo l’evento specifico, per esempio direzione,
coordinate, distanze e altri dati.
NOTA
È possibile simulare il doppio tap sul device mobile iOS tramite l’uso
dei tasti Shift+Option, per Android invece Shift+Ctrl .

Listato 11.1 main.dart


import 'package:flutter/material.dart';

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

class MyApp extends StatelessWidget {


const MyApp({super.key});

@override
Widget build(BuildContext context) {
return const MaterialApp(
title: 'Example',
home: GestureWidget(),
);
}
}
class GestureWidget extends StatefulWidget {
const GestureWidget({super.key});

@override
State<GestureWidget> createState() => _GestureWidgetState();
}

class _GestureWidgetState extends State<GestureWidget> {


bool _tapped = false;

@override
Widget build(BuildContext context) {
return Scaffold(
body: GestureDetector(
onTap: () {
setState(() {
_tapped = !_tapped;
});
},
child: Center(
child: Container(
decoration: BoxDecoration(border: Border.all()),
padding: const EdgeInsets.all(12),
child: Text('tap event: $_tapped'),
),
)),
);
}
}

L’esempio in questione è piuttosto semplice in quanto cattura


esclusivamente l’evento di Tap, tramite la callback definita dal
widget GestureDetector. La logica potrà essere definita
all’interno della callback di riferimento.

Advanced Gesture
Flutter offre un widget che consente di creare nuove gesture
custom. Il widget in questione è RawGestureDetector. Le
potenzialità offerte da questo widget consentono non solo di
creare gesture personalizzate che possono interagire con la UI,
ma anche combinarle tra loro.
Creiamo un esempio con RawGestureDetector che riproduce
il comportamento dell’esempio precedente. Lo scopo
dell’esempio è capire l’approccio legato all’uso di una factory.

Listato 11.2 main.dart


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

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

class MyApp extends StatelessWidget {


const MyApp({super.key});

@override
Widget build(BuildContext context) {
return const MaterialApp(
title: 'Example',
home: GestureWidget(),
);
}
}

class GestureWidget extends StatefulWidget {


const GestureWidget({super.key});

@override
State<GestureWidget> createState() => _GestureWidgetState();
}

class _GestureWidgetState extends State<GestureWidget> {


bool _tapped = false;

@override
Widget build(BuildContext context) {
return Scaffold(
body: RawGestureDetector(
gestures: {
TapGestureRecognizer:

GestureRecognizerFactoryWithHandlers<TapGestureRecognizer>(
() => TapGestureRecognizer(), //constructor
(TapGestureRecognizer instance) {
instance.onTap = () {
setState(() {
_tapped = !_tapped;
});
};
},
)
},
child: Center(
child: Container(
decoration: BoxDecoration(border: Border.all()),
padding: const EdgeInsets.all(12),
child: Text('tap event: $_tapped'),
),
)),
);
}
}

Il widget RawGestureDetector usa la key gestures per definire


una mappa. Utilizziamo in questo caso il tipo TapGestureRecognizer
e la factory GestureRecognizerFactoryWithHandlers. A sua volta, la
factory ha due parametri, il costruttore della classe
TapGestureRecognizer e la sua istanza. In quest’ultima definiamo la

nostra gesture, onTap.


NOTA
La factory è un costruttore che consente di introdurre una logica nella
creazione del costruttore, per esempio quella di non creare una nuova
istanza, ma utilizzarne una istanziata precedentemente.

Nel caso di una gesture custom avremmo potuto creare una


nuova classe al posto di TapGestureRecognizer, per
DragGestureRecognizer o MultiDragGestureRecognizer per
tracciare il puntatore sullo schermo.
È un argomento piuttosto avanzato e consiglio la lettura della
documentazione del widget
https://api.flutter.dev/flutter/widgets/RawGestureDetector-class.html.

InkWell
InkWell è un widget Material design molto simile a
GestureDetector ma, a differenza di questo, implementa una
“ripple animation” che viene scatenata appena invocata la
callback configurata. Inoltre, InkWell implementa meno gesture
rispetto a GestureDetector; per esempio, non implementa eventi
di tipo drag*.
Listato 11.3 main.dart
import 'package:flutter/material.dart';

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

class MyApp extends StatelessWidget {


const MyApp({super.key});

@override
Widget build(BuildContext context) {
return const MaterialApp(
title: 'Example',
home: InkWellWidget(),
);
}
}

class InkWellWidget extends StatefulWidget {


const InkWellWidget({super.key});

@override
State<InkWellWidget> createState() => _InkWellWidgetState();
}

class _InkWellWidgetState extends State<InkWellWidget> {


bool? _tapped;

@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: InkWell(
onTap: () {
setState(() {
_tapped = false;
});
},
onLongPress: () {
setState(() {
_tapped = true;
});
},
child: Container(
decoration: BoxDecoration(border: Border.all()),
padding: const EdgeInsets.all(8.0),
child: Text('onTap or LongPress: $_tapped'),
),
)),
);
}
}
Esattamente come per il widget GestureDetector, InkWell
definisce una serie di callback per gestire un set di gesture.
L’animazione definita come “ripple” si espande in maniera
concentrica dal punto esatto del tocco verso tutto lo spazio
occupato da InkWell.
NOTA
InkWell, rispetto a GestureDetector, consente di intercettare le gesture
anche nello spazio di padding.
Capitolo 12

Demo

Abbiamo introdotto e approfondito molti aspetti legati a Flutter,


tanto da poter pensare di creare la nostra prima app. In questo
capitolo faremo esattamente questo: metteremo in pratica
quanto appreso per creare un’applicazione vera e propria, che
vada oltre il singolo caso di studio. La nostra prima app
chiamata demo_meteo sarà composta da due viste, che ci
daranno la possibilità di consultare il meteo relativo alla nostra
posizione. Ogni vista gestirà questa feature in modo differente.
La home corrisponderà alla vista che offre un dettaglio sul
meteo giornaliero; l’altra vista, invece, sarà una panoramica
settimanale. Durante la fase di sviluppo, tratteremo i punti
fondamentali che finora mancavano nella lista di argomenti
propedeutici allo sviluppo di un’app in Flutter. Vedremo step by
step gli argomenti introdotti.
Iniziamo con il creare l’app: demo_meteo.
$ flutter create demo_meteo

Creating project demo_meteo...


Running "flutter pub get" in demo_meteo... 2'088ms
Wrote 127 files.

All done!
In order to run your application, type:

$ cd demo_meteo
$ flutter run

Your application code is in demo_meteo/lib/main.dart.


Sarebbe possibile creare l’app anche tramite VS Code.

Figura 12.1 VS Code -> Riquadro Comandi -> Flutter.

Figura 12.2 Nuovo progetto.

Come in Figura 12.1 e 12.2, terminare il “wizard” per avere


una demo funzionante, esattamente come avviene per il
comando flutter create…. Questo è possibile proprio perché VS
Code eseguirebbe lo stesso comando che eseguiamo da
console.
Flutter offre anche la possibilità di utilizzare dei campioni. Il
comando flutter create --list-samples test.json consente di
scaricare una lista di esempi con i relativi dettagli.
$ flutter create --list-samples test.json

Listato 12.1 test.json


[
...
{
"id": "material.NavigationBar.2",
"element": "NavigationBar",
"sourcePath": "lib/src/material/navigation_bar.dart",
"sourceLine": 72,
"channel": "stable",
"serial": "2",
"package": "flutter",
"library": "material",
"copyright": null,
"description": "This example shows a [NavigationBar] as it is
used within a [Scaffold]\nwidget when there are nested navigators that
provide local navigation. The\n[NavigationBar] has four
[NavigationDestination] widgets with different\ncolor schemes. The
[onDestinationSelected] callback changes the selected\nitem's index and
displays a corresponding page with its own local navigator\nin the body
of a [Scaffold].",
"file": "material.NavigationBar.2.dart"
},
{

Possiamo scegliere dalla lista un sample e procedere alla sua


installazione come base per sviluppare eventuali evoluzioni o,
ancor più semplicemente, prendere spunto.
$ flutter create --sample=material.NavigationBar.2 demo_meteo

API Platform
Flutter offre un’infinità di plugin che consentono, tra le tante
possibilità, di accedere alle API offerte dal sistema operativo.
NOTA
Il plugin differisce da un package perché ha al suo interno del codice
nativo che consente il dialogo con il platform di riferimento.

Sarebbe anche possibile creare un flusso tramite una o più


classi fornite da Flutter per dialogare in modalità asincrona con
le API del sistema operativo, per esempio tramite
MethodChannel per Android o FlutterMethodChannel per iOS.
Lo scopo di questo paragrafo non è spiegare come creare un
plugin, ma piuttosto come utilizzarne uno già esistente per
dialogare con il sistema operativo.

Geolocation
Nella nostra app vogliamo offrire la feature di
geolocalizzazione dell’utente. Geolocalizzare significa accedere
al GPS tracker del device, ovvero interagire con le API del
sistema operativo per catturare la posizione dell’utente.
La scelta più immediata è quella di utilizzare uno specifico
plugin, creato dalla community, che di fatto è diventato lo
standard per l’accesso alla geolocalizzazione.
Il plugin in questione è Geolocator
(https://pub.dev/packages/geolocator). Nella sezione score è
possibile vedere quanto effettivamente sia apprezzato e
utilizzato il plugin in questione.
NOTA
Ogni package presente in pub.dev viene catalogato con un sistema di
score. Lo score è composto da un numero di like, dei punti dati in
relazione alla qualità e manutenibilità del codice e, infine, un valore di
popolarità. Questi valori devono essere presi in considerazione per
valutare l’adozione del package.

L’installazione del plugin, come quella di tutti i package, può


essere condotta tramite il comando flutter.
$ flutter pub add geolocator

Al termine dell’installazione possiamo notare che il file


pubspec.yaml è stato modificato con l’aggiunta della riga "

geolocator: ^9.0.2" sotto la sezione dependencies.


Terminata l’installazione, non rimane altro che usarlo.

Permission
Nel momento in cui si usano le primitive offerte dal device per
ricavare, per esempio, informazioni relative alla posizione, è
necessario configurare l’applicativo per richiedere l’esplicito
permesso dell’utente.
Per questa ragione è necessario definire nella configurazione
dell’app, in relazione ai permessi, che cosa effettivamente l’app
richieda di essere abilitata a fare. Per l’accesso al GPS è
necessario definire la specifica permission e questa strada è
differente a dipendenza del sistema operativo.
Per Android, per esempio, è necessario modificare il file
AndroidManifest.xml, aggiungendo, allo stesso livello di

<application>, la definizione dei permessi necessari tramite la key


uses-permission. Il file AndroidManifest.xml è presente in
android/app/src/main/.

<manifest xmlns:android="http://schemas.android.com/apk/res/android"..

<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"
/>
<uses-permission
android:name="android.permission.ACCESS_COARSE_LOCATION" />

<application..

La stessa logica deve essere seguita per iOS. Per gestire i


permessi è necessario modificare il file Info.plist contenuto nella
directory ios/Runner:
<key>NSLocationWhenInUseUsageDescription</key>
<string>This app needs access to location when open.</string>
<key>NSLocationAlwaysUsageDescription</key>
<string>This app needs access to location when in the background.
</string>

La gestione dei permessi che abbiamo predisposto non


consente di gestire l’accesso ai dati GPS quando l’app è in
background. Questa feature necessità di passaggi aggiuntivi;
,nel caso fosse indispensabile vi invito ad approfondire nella
documentazione ufficiale:
https://developer.android.com/training/location/background

https://developer.apple.com/documentation/corelocation/handling_lo

cation_updates_in_the_background

Il primo passo per l’uso di Geolocator è quello di importare la


libreria:
import 'package:geolocator/geolocator.dart';

Ora, sarà possibile accedere a tutto il necessario per la


gestione della geolocalizzazione. Possiamo procedere con l’uso
di una funzione, gentilmente presa in prestito dalla
documentazione del package geolocator.
Future<Position> _determinePosition() async {
bool serviceEnabled;
LocationPermission permission;

serviceEnabled = await Geolocator.isLocationServiceEnabled();


if (!serviceEnabled) {
return Future.error('Location services are disabled.');
}

permission = await Geolocator.checkPermission();


if (permission == LocationPermission.denied) {
permission = await Geolocator.requestPermission();
if (permission == LocationPermission.denied) {
return Future.error('Location permissions are denied');
}
}

if (permission == LocationPermission.deniedForever) {
return Future.error(
'Location permissions are permanently denied, we cannot request
permissions.');
}

return await Geolocator.getCurrentPosition();


}

La funzione _determinePosition non fa altro che verificare se il


servizio GPS è attivo tramite il metodo isLocationServiceEnabled.
Nel caso in cui il servizio non sia attivo, è possibile gestire
questa casistica chiedendo all’utente gentilmente di attivare il
servizio o, nel caso in cui il metodo checkPermission torni
LocationPermission.denied, chiedere di configurare i permessi per
accedere al servizio.
Entrambe le casistiche possono essere gestite indirizzando
l’utente alle impostazioni del device tramite i metodi:
openAppSettings e openLocationSettings. Su Android, i suddetti metodi

apriranno le impostazioni di dettaglio relative allo stato del


servizio piuttosto che lo stato dei permessi. In iOS entrambi i
metodi portano l’utente alle impostazioni “generali” di sistema;
l’utente in autonomia dovrà navigare le impostazioni per attivare
il servizio GPS o configurare i permessi. Questa discrepanza tra
iOS e Android è causata da una differente implementazione di
sicurezza del sistema operativo, non dal plugin.
Infine, il metodo getCurrentPosition accede al dettaglio della
posizione dell’utente.
Proseguiamo introducendo una logica nuova rispetto a quanto
offerto di default da Flutter, per poter accedere alla posizione
dell’utente.

Listato 12.2 main.dart


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

void main() {
runApp(const MyApp());
}

class MyApp extends StatelessWidget {


const MyApp({super.key});

@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: const MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}

class MyHomePage extends StatefulWidget {


const MyHomePage({super.key, required this.title});
final String title;

@override
State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {


String? _position;

@override
void initState() {
super.initState();
}

void savePosition() async {


Position position = await _determinePosition();
setState(() {
_position = '${position.latitude} - ${position.longitude}';
});
}

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
const Text(
'Your position:',
),
Text(_position ?? "waiting.."),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: savePosition,
tooltip: 'Increment',
child: const Icon(Icons.add),
),
);
}
}

Future<Position> _determinePosition() async {


bool serviceEnabled;
LocationPermission permission;

serviceEnabled = await Geolocator.isLocationServiceEnabled();


if (!serviceEnabled) {
return Future.error('Location services are disabled.');
}

permission = await Geolocator.checkPermission();


if (permission == LocationPermission.denied) {
permission = await Geolocator.requestPermission();
if (permission == LocationPermission.denied) {
return Future.error('Location permissions are denied');
}
}

if (permission == LocationPermission.deniedForever) {
return Future.error(
'Location permissions are permanently denied, we cannot request
permissions.');
}

return await Geolocator.getCurrentPosition();


}

Appena eseguita l’app, “tappando” sul floatingActionButton


verrà chiesto all’utente di consentire l’accesso alla location e,
infine, verrà aggiornato il widget con la posizione.
Figura 12.3 Nuovo screen.

NOTA
Sia il simulatore Android che iOS consentono di gestire la
localizzazione ed è possibile, inoltre, configurare una posizione
custom, navigando le impostazioni del simulatore. In Android, è
possibile farlo utilizzando la barra di impostazioni del device virtuale e
accedendo al menu location; in iOS, dal menu Simulator (Debug ->
Location -> Custom Location).
Figura 12.4 permission (iOS e Android).
Figura 12.5 Nuova posizione.

Custom Icons
Nel caso in cui le icone offerte da Flutter non dovessero
soddisfare le nostre esigenze, è possibile comunque utilizzarne
altre. Fra varie possibilità, sicuramente il primo step è cercare
qualcosa che possa fare al caso nostro dal sito FlutterIcon
(https://www.fluttericon.com/). FlutterIcon è un sito che consente di
generare icone partendo da file SVG e anche di accedere a un
vasto catalogo per attingere alle icone create da terzi.
Al caso nostro, esiste nel catalogo un set di icone a tema
meteo: Meteocons. Procediamo al download; una volta
terminato e scompattato il file zip, è necessario indicare l’uso del
“nuovo” font nel file pubspec.yaml, per informare Flutter della
presenza di un nuovo font.
Modifichiamo il file aggiungendo la configurazione relativa al
font; inoltre, dobbiamo indicare il path in cui abbiamo
posizionato il file MyFlutterApp.ttf, in questo caso la directory
fonts.

flutter:

fonts:
- family: MyFlutterApp
fonts:
- asset: fonts/MyFlutterApp.ttf

L’uso delle icone custom necessita di una classe che è


presente nell’archivio scaricato: my_flutter_app_icons.dart.
Copiamo il file in questione e lo posizioniamo all’interno della
directory lib e, per essere più ordinati possibile, nella
sottodirectory icons, opportunamente creata.
Terminata la configurazione, per procedere al suo utilizzo è
necessario importare il file in questione e utilizzare la classe
fornita: MyFlutterApp.
Per esempio:
import './icons/my_flutter_app_icons.dart';

Icon(MyFlutterApp.wind)

Ricapitolando: il file MyFlutterApp.ttf in demo_meteo/fonts, il file


my_flutter_app_icons.dart in demo_meteo/lib/icons.

Http
Credo sia pressoché impossibile immaginare una qualsiasi
applicazione che non faccia uso del networking. Il package più
usato per implementare richieste http è sicuramente l’omonimo
http (https://pub.dev/packages/http).
Procediamo innanzitutto all’installazione. Al solito:
$ flutter pub add http

Verrà aggiornato il file pubspec.yaml e per l’utilizzo della classe


http sarà necessario importare la libreria.
import 'package:http/http.dart';

L’uso della libreria http passa inizialmente per la definizione


dell’URI, che nel nostro caso è un URL.
NOTA
Il termine URI (Uniform Resource Identifier) indica una risorsa a cui è
possibile accedere tramite una stringa. Nel caso di una risorsa https la
URI è di tipo URL.

La classe URI ci consente di strutturare con dovizia il nostro


URL; per esempio, è possibile definire lo schema: https/http, il
path ed eventuali parametri da passare in query string, insomma
tutto il necessario per predisporre la “request”. Una volta
predisposto l’oggetto uri, possiamo procedere alla chiamata
tramite i metodi http implementati dalla libreria, per esempio:
delete, get, head, patch, post e put.
Nella demo che realizzeremo, sarà necessario invocare un
servizio meteo free, che consente, tramite chiamate http e il
metodo get, di accedere alle previsioni meteo. Il servizio
utilizzato dalla demo è offerto da: https://open-meteo.com/.
NOTA
Json, acronimo di JavaScript Object notation, è un formato di tipo
testuale utilizzato per lo scambio di dati tra client/server. Nato da
JavaScript, ha una sintassi che richiama gli oggetti Javascript: una
mappa chiave valore. Es: {"name": "Pippo","mother": "Liviana"}

L’URL da invocare sarà:


https://api.open-meteo.com/v1/forecast?
latitude=${location?.latitude}&longitude=${location?.longitude}&current_
weather=true,

passeremo come parametri la latitudine e longitudine della


nostra posizione. Un esempio di classe che riproduce quanto
appena detto è: _CurrentWeatherViewState:
class _CurrentWeatherViewState extends State<CurrentWeatherView> {
Future<Weather> fetchCurrentWeather() async {
Uri url = Uri.parse(
'https://api.open-meteo.com/v1/forecast?
latitude=${widget.location?.latitude}&longitude=${widget.location?.longi
tude}&current_weather=true');
final response = await http.get(url);

if (response.statusCode == 200) {
return jsonDecode(response.body);
} else {
throw Exception('Failed to load weather');
}
}

In questo caso, all’interno della classe _CurrentWeatherViewState


definiamo un metodo fetchCurrentWeather che crea l’URL tramite il
metodo parse. Creata la request url possiamo invocare il metodo
get per inviare la request. La property statusCode di response
consente di intercettare un caso di successo piuttosto che un
eventuale errore. La funzione jsonDecode consente di ottenere un
oggetto json-like, in genere una mappa tramite la quale si
possono analizzare e implementare le logiche sul body della
response.

Struttura
Il design che vorremmo applicare nella nostra demo_meteo
sarebbe composto da una bottomNavigation Bar, in cui sarà
possibile scegliere due differenti view: in una, accedere al
meteo corrente, nell’altra, accedere alle previsioni meteo dei
sette giorni successivi.
Procederemo strutturando il nostro codice sorgente, per
intenderci ciò che è contenuto all’inteno della directory lib, in più
file. Rispetto a quanto visto finora, dove tutto il nostro sorgente
era contenuto nel file main.dart, qui faremo un passo avanti.
Strutturare il codice sorgente in Flutter, così come in altri
framework, è di vitale importanza, poiché UI e la relativa logica
sono contenuti nello stesso codice sorgente e, quando la nostra
applicazione si espande, o ancor più semplicemente più
sviluppatori dovranno collaborare allo sviluppo del codice, si
rende necessario che il codice sia ben strutturato. La
manutenibilità è alla base di questo concetto e un codice
organizzato migliora di molto la sua leggibilità e aiuta non poco
nella risoluzione di eventuali bug o refactoring.
Il primo passo per strutturare un’app Flutter con un buon
livello di leggibilità è partire dall’assunzione che il file main.dart
dovrà contenere meno logica possibile.
main.dart
Il file main.dart verrà modificato per contenere meno codice
possibile. Rispetto a quanto offerto dalla demo di Flutter, lo
modificheremo per contenere solo il widget stateless root,
MaterialApp, nel nostro caso.
import 'package:flutter/material.dart';
import './screens/home_screen.dart';

void main() {
runApp(const MyApp());
}

class MyApp extends StatelessWidget {


const MyApp({super.key});

@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Demo Meteo',
theme: ThemeData(
primarySwatch: Colors.blueGrey,
),
home: const MyHomePage(title: 'Demo Meteo'),
);
}
}

Ognuno è libero di approcciare il codice e la struttura come


meglio crede, d’altronde il concetto di “codice strutturato” è
piuttosto soggettivo e non c’è una regola imposta dal framework
e nessun pattern obbligatorio. Tuttavia, sono due le tipologie di
approcci che si possono seguire e in genere non sono neanche
strettamente legate a Flutter, ma più in generale allo sviluppo di
applicativi che non hanno particolari restrizioni sull’uso di design
pattern specifici. I due approcci prevedono di strutturare il
codice sulla base del tipo piuttosto che sulla base della feature.
Mi spiego meglio.
Nel primo caso, l’approccio è “by type” e avremo una struttura
di questo tipo:
lib/
screens/
widgets/
models/
services/
..

Nel secondo caso, definito come “by feature”, la struttura sarà


così differente:
lib/
feature1
screens/
widgets/
models/
services/
..
feature2
screens/
widgets/
models/
services/
..

Il primo pattern, come possiamo intuire, prevede di dividere il


sorgente per tipologia. Tutti i services dentro la directory
services, lo stesso per i modelli ecc. Per quanto sia più

immediato, questo approccio è meno consigliato per


applicazioni di medie dimensioni. Immaginiamo se avessimo
trenta file per directory: sicuramente non sarebbe efficiente la
ricerca di una porzione di codice specifica.
Nel secondo caso, l’organizzazione del sorgente è divisa per
funzionalità. Nel caso di applicazioni importanti, certamente
questo approccio è il migliore.
Il mio consiglio è quello di iniziare per gradi. Difficilmente le
prime applicazioni sono di dimensioni tali da farvi propendere
verso un design by feature. Detto ciò, consiglio di iniziare
organizzando il codice con queste directory.
Themes: conterrà i temi, light e/o dark.
Screens: la directory screens potrà essere usata per
contenere i widget che compongono le nostre
view/screen/page.
Widgets: potrà contenere i widget che sono utilizzati e
condivisi tra più screen.
Utils: potrà contenere funzioni a uso generico.
Models: potrà contenere un data model.
Services: conterrà i servizi, per esempio quelli per invocare
servizi http.
Più il software sarà complesso, più avremo necessità di una
struttura più ampia, ma al momento direi che abbiamo quanto
necessario.

screens/home_screen.dart
Il file in questione corrisponde al widget home di MaterialApp.

Listato 12.3 home_screen.dart


import 'package:flutter/material.dart';
import 'package:geolocator/geolocator.dart';
import 'current_screen.dart';
import 'forecast_screen.dart';
import '../services/get_position.dart';
import '../icons/my_flutter_app_icons.dart';

class MyHomePage extends StatefulWidget {


const MyHomePage({super.key, required this.title});
final String title;

@override
State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {


int _selectedIndex = 0;
late Future<Position> _position;

Widget _onWidgetTapped(Position data) {


if (_selectedIndex == 0) {
return CurrentWeatherView(location: data);
} else {
return ForecastWeatherView(location: data);
}
}

void _onItemTapped(int index) {


setState(() {
_selectedIndex = index;
});
}

@override
void initState() {
super.initState();
_position = determinePosition();
}

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: FutureBuilder<Position>(
future: _position,
builder: (BuildContext context, AsyncSnapshot<dynamic>
snapshot) {
if (snapshot.hasData) {
return _onWidgetTapped(snapshot.data);
} else {
return const Text('waiting location..');
}
})),
bottomNavigationBar: BottomNavigationBar(
items: const <BottomNavigationBarItem>[
BottomNavigationBarItem(
icon: Icon(MyFlutterApp.sun),
label: 'Weather',
),
BottomNavigationBarItem(
icon: Icon(Icons.calendar_month),
label: 'Days',
),
],
currentIndex: _selectedIndex,
selectedItemColor: Colors.amber[800],
onTap: _onItemTapped,
),
);
}
}
In questo file, creiamo il widget MyHomePage che è la parte
principale del nostro widget tree. Il widget in questione è uno
stateful, in quanto dovrà gestire i dati relativi alla
geolocalizzazione, e implementa anche una
NavigationBottomBar per gestire le due view. La prima
(Weather) mostra il widget CurrentWeatherView, l’altra (Days) il
widget ForecastWeatherView. Un aspetto a cui dare importanza
è sicuramente il widget FutureBuilder, che ci aiuta a integrare
nel nostro layout dati di tipo Future.

FutureBuilder
FutureBuilder consente di procedere alla build di un widget
che ha come dipendenza un dato asincrono. È importante
ricordare che la funzione build di un widget è chiamata N volte
per N situazioni diverse, cosa ben diversa, per esempio, per il
metodo initState. È cosa buona e giusta risolvere la Future, per
esempio dentro il metodo initState, e utilizzare il valore di ritorno
della Future all’interno del widget FutureBuilder.
Nel nostro esempio il widget FutureBuilder è usato per gestire
la chiamata asincrona per ottenere la posizione. Avremmo
potuto ottenere il nostro scopo anche invocando direttamente la
funzione:
child: FutureBuilder<Position>(
future: determinePosition(),
builder: (BuildContext context, AsyncSnapshot<dynamic>
snapshot) {

Un approccio di questo tipo è senz’altro sconsigliato in quanto


avrebbe spinto Flutter a invocare la funzione determinePosition
ogni qual volta che il metodo build fosse stato invocato.
Una volta risolta la Future, AsyncSnapshot<dynamic> snapshot
consente di accedere alla snapshot, ovvero alla Future ritornata
da determinePosition. Inoltre, AsyncSnapshot consente di
accedere agli stati della Future per intercettare la sua
risoluzione. Nel nostro caso, tramite snapshot.hasData, ci
accertiamo di accedere al dato atteso e risolto.
L’uso di FutureBuilder è pressoché indispensabile nelle
casistiche in cui i nostri widget conterranno dati che dovranno
essere reperiti da routine asincrone.
La funzione determinePosition() potrà essere invocata grazie
all’import del file determinePosition che abbiamo posizionato in un
file separato sotto la directory services.
Rispetto a quanto visto appena sopra, nel paragrafo
Geolocation, è necessario rinominare la funzione da
_determinePosition a determinePosition, in quanto non dobbiamo

dimenticare che il carattere ‘_’ all’inizio di una classe, metodo,


funzione o property la rende privata, quindi non richiamabile
all’esterno del file .dart in cui è definita.

services/get_position.dart
Listato 12.4 get_position.dart
import 'package:geolocator/geolocator.dart';

Future<Position> determinePosition() async {


bool serviceEnabled;
LocationPermission permission;

serviceEnabled = await Geolocator.isLocationServiceEnabled();


if (!serviceEnabled) {
return Future.error('Location services are disabled.');
}

permission = await Geolocator.checkPermission();


if (permission == LocationPermission.denied) {
permission = await Geolocator.requestPermission();
if (permission == LocationPermission.denied) {
return Future.error('Location permissions are denied');
}
}

if (permission == LocationPermission.deniedForever) {
return Future.error(
'Location permissions are permanently denied, we cannot request
permissions.');
}

return await Geolocator.getCurrentPosition();


}

StreamBuilder
StreamBuilder è un widget che, una volta introdotto e
compreso il funzionamento di FutureBuild, si applica allo stesso
modo per l’oggetto Stream.
Il widget StreamBuilder ascolta i cambiamenti dell’oggetto
Stream e, quando questi avvengono, Flutter esegue la build del
widget. Il comportamento è identico a quello di FutureBuilder. La
differenza intrinseca è che da uno Stream ci si aspetta che il
numero di eventi asincroni sia per definizione maggiore di
Future e di conseguenza la gestione degli stati è leggermente
più complessa.
Un esercizio utile potrebbe essere quello di provare a
sostituire FutureBuilder con StreamBuilder, modificando di
conseguenza il sorgente services/get_position.dart, per poi
spingersi a inviare la posizione ogni N secondi. Sarebbe
sicuramente un buon caso di studio.

screens/current_screen.dart
Come abbiamo visto nel file home_screen, è necessario definire i
due widget che corrispondono alle due tab. In questo caso,
procediamo definendo la tab Weather, che è la tab di default.

Listato 12.5 current_screen.dart


import '../services/open_meteo.dart';
import '../models/weather.dart';
import 'package:flutter/material.dart';
import 'package:geolocator/geolocator.dart';

class CurrentWeatherView extends StatefulWidget {


final Position location;
const CurrentWeatherView({super.key, required this.location});

@override
State<CurrentWeatherView> createState() => _CurrentWeatherViewState();
}

class _CurrentWeatherViewState extends State<CurrentWeatherView> {


late Future<Weather> _futureCurrentWeather;

@override
void initState() {
super.initState();
_futureCurrentWeather = fetchCurrentWeather(
latitude: widget.location.latitude,
longitude: widget.location.longitude);
}

@override
Widget build(BuildContext context) {
return FutureBuilder<dynamic>(
future: _futureCurrentWeather,
builder: (BuildContext context, AsyncSnapshot<dynamic> snapshot) {
if (snapshot.hasData) {
return Card(
child: Column(mainAxisSize: MainAxisSize.min, children:
<Widget>[
ListTile(
title: Icon(
IconData(snapshot.data.codeIcon, fontFamily:
'MyFlutterApp'),
size: 62),
subtitle: Column(children: [
const SizedBox(height: 8),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Text('Temperature: '),
Text(
'${snapshot.data.temperature}°',
style: const TextStyle(
fontSize: 22, fontWeight: FontWeight.bold),
),
],
),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Text('Wind speed: '),
Text(
'${snapshot.data.windspeed} Km/h',
style: const TextStyle(
fontSize: 16, fontWeight: FontWeight.bold),
),
],
),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Text('Wind direction: '),
Text(
'${snapshot.data.winddirection}',
style: const TextStyle(
fontSize: 14, fontWeight: FontWeight.bold),
),
],
),
Text('Time: ${snapshot.data.time}'),
Text('Location: ${widget.location}'),
]),
)
]));
} else if (snapshot.hasError) {
return const Text('Error');
}
return const CircularProgressIndicator();
},
);
}
}

Il widget CurrentWeatherView ha la stessa logica vista


precedentemente, ovvero per il suo build è necessario eseguire
una routine asincrona. La posizione è già stata ottenuta, ed è
passata alla classe CurrentWeatherView tramite parametri del
suo costruttore nel file home_screen.dart. Al suo interno, però,
dobbiamo ottenere il meteo corrente.
La funzione per ottenere i dati meteo è chiamata
fetchCurrentWeather ed è necessario passare a questa latitudine e

longitudine, che serviranno a invocare correttamente l’API. La


funzione fetchCurrentWeather, così come avviene per
determinePosition, è stata definita in un file separato sotto la
directory services.
Figura 12.6 View Weather (iOS e Android).

services/open_meteo.dart
Il file open_meteo implementa due funzioni per gestire le due
request ognuno per le due differenti tab.

Listato 12.6 open_meteo.dart


import 'dart:convert';

import 'package:http/http.dart' as http;


import '../models/weather.dart';

Future<Weather> fetchCurrentWeather(
{required double latitude, required double longitude}) async {
Uri url = Uri.parse(
'https://api.open-meteo.com/v1/forecast?
latitude=$latitude&longitude=$longitude&current_weather=true');
final response = await http.get(url);

if (response.statusCode == 200) {
return Weather.fromJson(jsonDecode(response.body)
['current_weather']);
} else {
throw Exception('Failed to load weather');
}
}

Future<List<Weather>> fetchForecastWeather(
{required double latitude, required double longitude}) async {
List<Weather> w = [];
Uri url = Uri.parse(
'https://api.open-meteo.com/v1/forecast?
latitude=$latitude&longitude=$longitude&daily=temperature_2m_max,tempera
ture_2m_min,weathercode,winddirection_10m_dominant,windspeed_10m_max&tim
ezone=GMT');
final response = await http.get(url);

if (response.statusCode == 200) {
var j = jsonDecode(response.body);
for (int i = 0; i < j['daily']['time'].length; i++) {
w.add(Weather(
time: j['daily']['time'][i],
code: j['daily']['weathercode'][i],
codeIcon: Weather.iconFromCode(j['daily']['weathercode'][i]),
temperatureMin: j['daily']['temperature_2m_min'][i],
temperatureMax: j['daily']['temperature_2m_max'][i],
windspeed: j['daily']['windspeed_10m_max'][i],
winddirection: Weather.convertDirection(
j['daily']['winddirection_10m_dominant'][i].toDouble())));
}
return w;
} else {
throw Exception('Failed to load items');
}
}

L’import della libreria dart:convert consente l’accesso alla


funzione jsonDecode. Le funzioni fetchCurrentWeather e
fetchForecastWeather si occupano di recuperare i dati dal provider
open-meteo.com. Entrambe le funzioni si occupano anche di creare
l’oggetto Weather, basandosi sul data model definito in
model/weather.dart. Anche in questo caso suggerisco di

ristrutturare il file per utilizzare una classe, invece di usare due


singole funzioni. Un approccio di questo tipo aiuterebbe anche a
strutturare meglio i test: il perché sarà chiarito nel Capitolo 13,
dedicato ai test.

Modello dati
Il data model è uno degli aspetti cardini della
programmazione. L’uso che viene fatto di un data model, in
genere, è quello di validare e trasformare un dato acquisito
tramite la modellazione di un nuovo tipo di dato, nel nostro caso
weather.
La classe Weather mappa e consente di creare strutture dati
affinché possano essere “portate”, attraverso un modello, sulla
view, in modo strutturato e sicuro. Questo concetto è
sostanzialmente quello che viene definito come design pattern
MVC. Model, view e controller. Nel caso dell’app demo meteo, il
model è definito dal file model/weather.dart, la view dal file
contenuto nella directory screen e, se avessimo definito una
logica di manipolazione dei dati, avremmo potuto creare un file
separato dove sviluppare le business logic. Nel nostro esempio,
abbiamo solo una logica che riguarda la visualizzazione dei dati;
il nostro pattern non è propriamente un MVC, in quanto la logica
legata al dato è contenuta nello stesso file che definisce la view.
NOTA
Il package mvc_pattern (https://pub.dev/packages/mvc_pattern)
consente di implementare con estrema facilità un pattern MVC. La
scelta è soggettiva, ma consiglio la visione del codice di esempio per
rendersi immediatamente conto di come è realizzata una
implementazione di questo tipo.

models/weather.dart
Il file models/weather.dart implementa il modello dati
protagonista dell’applicazione.

Listato 12.7 models/weather.dart


class Weather {
final int code;
final int? codeIcon;
final double? temperature;
final double? temperatureMin;
final double? temperatureMax;
final double windspeed;
final String winddirection;
final String time;

const Weather({
required this.code,
this.codeIcon,
this.temperatureMin,
this.temperatureMax,
this.temperature,
required this.windspeed,
required this.winddirection,
required this.time,
});

factory Weather.fromJson(Map<String, dynamic> json) {


return Weather(
code: json['weathercode'],
codeIcon: iconFromCode(json['weathercode']),
temperature: json['temperature'],
temperatureMin: json['temperature_2m_min'],
temperatureMax: json['temperature_2m_max'],
windspeed: json['windspeed'] ?? json['windspeed_10m_max'],
winddirection: convertDirection(
json['winddirection'] ?? json['winddirection_10m_dominant']),
time: json['time'],
);
}
static String convertDirection(double direction) {
if ((direction >= 0 && direction <= 23) ||
(direction >= 337 && direction <= 360)) {
return 'North';
}
if ((direction >= 24 && direction <= 67)) {
return 'North-east';
}
if ((direction >= 68 && direction <= 113)) {
return 'East';
}
if (direction >= 293 && direction <= 336) {
return 'Nord-West';
}
if ((direction >= 247 && direction <= 292)) {
return 'West';
}
if ((direction >= 157 && direction <= 203)) {
return 'South';
}

if ((direction >= 114 && direction <= 156)) {


return 'South-east';
}

if ((direction >= 204 && direction <= 246)) {


return 'South-West';
}
throw 'Invalid direction';
}

static int iconFromCode(int code) {


if (code >= 0 && code <= 3) {
return 0xe80e;
}

if (code >= 45 && code <= 48) {


return 0xe820;
}

if (code >= 51 && code <= 57) {


return 0xe812;
}

if ((code >= 61 && code <= 67 || code >= 80 && code <= 82)) {
return 0xe813;
}
if ((code >= 71 && code <= 77 || code >= 85 && code <= 86)) {
return 0xe829;
}
if (code >= 61 && code <= 67) {
return 0xe813;
}
if (code >= 95 && code <= 99) {
return 0xe823;
}
throw 'Invalid code';
}
}

La classe Weather consente di mappare i dati ricevuti dalle


api implementate in services/open_meteo.dart e di creare un
modello. La classe Weather definisce il modello tramite le sue
properties, inoltre implementa un costruttore, due metodi statici
per la gestione dell’input e una factory.
Le due funzioni convertDirection e iconFromCode hanno lo scopo di
tradurre il dato input fornito dall’api in un linguaggio di più facile
lettura per l’utente, pertanto il dato viene manipolato per essere
visualizzato sulla user interface. Per esempio, tradurre il
wheatercode (WMO Weather interpretation codes) in un’icona o
convertire la direzione del vento in punti cardinali sono aspetti
legati al modello dati che in questo caso sono implementati
all’interno della classe. Invece di questa struttura, avremmo
potuto creare un file separato per implementare singole funzioni,
per esempio definendo una nuova directory chiamata utils.
Sono scelte soggettive e dipendono dalla complessità del dato
da gestire.
In questo caso il razionale per la scelta di rendere queste due
funzioni metodi statici della classe Weather è che comunque
sono logiche strettamente legate al dato Weather e soprattutto
non necessitano di essere utilizzate anche da altre classi.
Rimane valida la considerazione che renderle i metodi
convertDirection e iconFromCode a uso generico, come appunto

“utils”, sarebbe stata una scelta ugualmente sensata.


Infine, il metodo fromJson consente di creare l’istanza della
classe partendo da un json e di conseguenza tornare l’oggetto
Weather, tramite la keyword factory. Credo che sia necessario
spendere due parole in più sul tema factory.

Factory
La factory è un costrutto speciale che, rispetto a un costrutto
classico, aggiunge nuove possibilità. Diversamente dal costrutto
classico che si “limita” a restituire un nuova istanza di una
classe, l’uso di factory consente non solo la possibilità di
istanziare una nuova classe, ma anche di restituire un’istanza
già esistente (cache), piuttosto che restituire un sottotipo di
classe; insomma, l’uso di factory introduce nuove features.
Nel sorgente models/weather, utilizziamo il costrutto per
istanziare una nuova classe, ma è anche possibile utilizzare
fromType per istanziare una nuova classe direttamente tramite un

json, piuttosto che definendo i singoli membri. L’esigenza nasce


dalla response diversa tra le due chiamate al provider open-
meteo e con una factory implementiamo una differente logica
rispetto al costruttore. Inoltre, se fosse necessario, sarebbe
anche possibile introdurre delle condizioni, ma in questo caso
non sono necessarie. A onor del vero, pesa su questa decisione
implementativa anche l’occasione di mostrare entrambe le
soluzioni, l’uso del costruttore e l’introduzione del concetto di
factory. Rimane valido il concetto che usare una factory resta un
approccio migliore in quanto il modello potrebbe variare ed
estendersi e il servizio rimarrebbe agnostico.
Un esempio in cui utilizziamo factory per gestire una
sottoclasse:
class Station extends Weather {...}

class Weather {
Weather();
factory Weather.fromType(String type) {
if (type == 'weather') return Weather();
if (type == 'station') return Station();
throw "error factory";
}
}

Un esempio in cui gestiamo una cache.


class Weather {
int idStation;
Weather._internal(this.idStation);
static Map<int, Weather> _cache = <int, Weather>{};

factory Weather(int idStation) {


if (_cache.containsKey(idStation))
return _cache[idStation]!;
else
return _cache[idStation] = Weather._internal(idStation);
}
}

void main() {
print(Weather(1).hashCode);
print(Weather(1).hashCode);
}

Il log ci mostra come effettivamente le due istanze siano le


stesse.
NOTA
All’interno di una classe è possibile definire più costruttori e questi
vengono chiamati named costructor. Un esempio è il costruttore
definito come _internal.

NOTA
Hashcode è una property che accompagna ogni oggetto e lo identifica
univocamente.

Un esempio con cui creiamo una classe Singleton.


class Weather {
Weather._internal();

static final Weather _instance = Weather._internal();

factory Weather() {
return _instance;
}
}
void main() {
print(Weather().hashCode);
print(Weather().hashCode);
}

Anche in questo caso il log ci mostrerà che le due istanze


sono le stesse.
NOTA
Singleton è un design pattern che viene usato per definire una classe
che avrà un’unica e singola istanza.

screens/forecast_screen.dart
La view che si basa sul widget ForecastWeatherView
implementa una logica leggermente diversa dalla view
precedente.

Listato 12.8 forecast_screen.dart


import '../models/weather.dart';
import 'package:flutter/material.dart';
import 'package:geolocator/geolocator.dart';
import '../services/open_meteo.dart';

class ForecastWeatherView extends StatefulWidget {


final Position location;
const ForecastWeatherView({super.key, required this.location});

@override
State<ForecastWeatherView> createState() =>
_ForecastWeatherViewState();
}

class _ForecastWeatherViewState extends State<ForecastWeatherView> {


late Future<List<Weather>> futureForecastWeather;
List<Weather> w = [];

@override
void initState() {
super.initState();
futureForecastWeather = fetchForecastWeather(
latitude: widget.location.latitude,
longitude: widget.location.longitude);
}

@override
Widget build(BuildContext context) {
return FutureBuilder<dynamic>(
future: futureForecastWeather,
builder: (BuildContext context, AsyncSnapshot<dynamic> snapshot) {
if (snapshot.hasData) {
debugPrint('Future result: ${snapshot.data}');
return ListView(
padding: const EdgeInsets.all(8),
children: snapshot.data
.map<Widget>((Weather i) => Card(
child:
Column(mainAxisSize: MainAxisSize.max,
children: [
ListTile(
title: Row(
mainAxisAlignment:
MainAxisAlignment.center,
children: [
Icon(
IconData(i.codeIcon ?? i.code,
fontFamily: 'MyFlutterApp'),
size: 62),
Text(i.time,
style: const TextStyle(
color: Color.fromRGBO(0, 0, 0,
0.8),
fontSize: 12,
fontWeight: FontWeight.bold))
]),
subtitle: Column(children: [
const SizedBox(height: 8),
Row(
mainAxisAlignment:
MainAxisAlignment.center,
children: [
const Text('Temperature min: '),
Text(
'${i.temperatureMin}°',
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold),
),
const Text(' max: '),
Text(
'${i.temperatureMax}°',
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold),
),
],
),
Row(
mainAxisAlignment:
MainAxisAlignment.center,
children: [
const Text('Wind speed: '),
Text(
'${i.windspeed} Km/h',
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold),
),
],
),
Row(
mainAxisAlignment:
MainAxisAlignment.center,
children: [
const Text('Wind direction: '),
Text(
'${i.winddirection} Km/h',
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.bold),
),
],
),
Text('Location: ${widget.location}'),
]),
)
])))
.toList());
} else if (snapshot.hasError) {
return const Text('Error');
}
return const CircularProgressIndicator();
},
);
}
}

I dati vengono reperiti tramite la funzione fetchForecastWeather.


Questa funzione, rispetto a fetchCurrentWeather, non fa uso della
factory, semplicemente perché la struttura dati ritornata è
leggermente differente e non volevo complicare il codice
creando una funzione di mapping per utilizzare la factory
fromJson anche per questa casistica. Inoltre, differenziare ed

introdurre la creazione dell’istanza tramite il costrutto classico


non mi dispiaceva; insomma vale quanto detto prima. In questo
caso abbiamo utilizzato entrambi i metodi che la classe Weather
offre.
La funzione fetchForecastWeather torna una lista di istanze.
Questo perché la view “days” mostra una previsione di sette
giorni. Lo state _ForecastWeatherViewState dovrà preoccuparsi di
renderizzare e gestire una lista di widget Card, dove ogni Card
mostrerà il dettaglio della previsione di giornata. L’iterazione
degli oggetti Weather è gestita tramite il metodo map.
"snapshot.data
.map<Widget>((<Weather>i) => Card(...))..toList()"
Figura 12.7 View Days (iOS e Android).

Ci sono diverse strade per l’iterazione di una lista: potremmo


approcciarla anche con il costrutto for-in (for ( var i in Widget ))
piuttosto che l’uso del metodo .map con .toList(). Ho utilizzato il
metodo più ostico da leggere solo per rendere tutto più
frizzante.
All’interno dell’iterazione faremo riferimento alle singole
istanze di Weather tramite l’uso del counter i, per esempio:
i.codeIcon, i.code e così per le altre properties. Infine, il widget

ListView rende la view scrollable, consentendo così di mostrare


con semplicità tutti i sette giorni.

Release
Se volessimo testare sul device fisico una build più vicina a
quella che vorremmo distribuire, senza alcun dubbio dobbiamo
procedere a una build di release. La build di release è
caratterizzata da una dimensione contenuta poiché vengono
eseguite una serie di ottimizzazioni e non porta con sé tutti gli
strumenti di debug a cui ci siamo appoggiati in fase di sviluppo.
È possibile procedere tramite il comando Flutter.
Procediamo con una build per Android.
$ flutter run –release

Using hardware rendering with device sdk gphone64 x86 64. If you notice
graphics artifacts, consider enabling software rendering with "--enable-
software-rendering".
Launching lib/main.dart on sdk gphone64 x86 64 in release mode...
Running Gradle task 'assembleRelease'... 2'109ms
✓ Built build/app/outputs/flutter-apk/app-release.apk (7.5MB).

Flutter run key commands.


h List all available interactive commands.
c Clear the screen
q Quit (terminate the application on the device).
D/FlutterGeolocator( 8920): Creating service.
D/FlutterGeolocator( 8920): Binding to location service.
D/FlutterGeolocator( 8920): Flutter engine connected. Connected engine
count 1

La sorpresa a cui andremo incontro è senza alcun dubbio un


“Error”, dopo aver dato il permesso all’uso del GPS. Perché
accade questo ? Perché è necessario esplicitare che la nostra
applicazione fa uso di “Internet”, questo è necessario proprio
per l’uso delle API fornite da open-meteo.
È possibile accedere ad log, modificando il sorgente:
} else if (snapshot.hasError) {
return Text('Error: ${snapshot.error.toString()}');
}

Una volta appurato che l’errore è relativo alla connessione


internet procediamo ad inserire la permission specifica nel file:
AndroidManifest.xml.
<uses-permission android:name="android.permission.INTERNET" />

Per iOS, il problema non sussiste e non è necessario


aggiungere nessuna policy specifica, purtroppo per iOS non è
possibile procedere con la modalità –release con il simulatore,
sarà necessario utilizzare un device fisico.

iOS
Eseguire una build su un device fisico iOS è più ostico del
corrispettivo in Android. Seguiamo brevemente gli step da
seguire:
verificare il device, non appena collegato al desktop;
aprire Xcode e seguire il menu Runner -> Signing and
Capabilities -> Team e procedere con l’apple ID (email);
dopo la login, viene creato un Personal Team e potete
selezionarlo direttamente dalle impostazioni dell’account;
abilitare il Developer mode sul device (in Privacy e
Sicurezza);
eseguire la build in Xcode (genererà un errore sul device in
fase di run poichè l’account developer non è verificato:
“Untrusted Developer”);
procedere con il trust dell’account per l’app demo_meteo in
Generali -> VPN e gestione dispositivi -> App sviluppatore;
eseguire l’app.
Se incontraste qualche difficoltà, l’invito è di seguire la guida
di Flutter: https://docs.flutter.dev/deployment/ios.
NOTA
In VS Code, nel menu Esegui è possibile configurare la tipologia di
build tramite il file launch.json. Sarà possibile scegliere nel tab Run la
tipologia di profilo, una volta che ne sarà stata creata una per la
modalità “release”.

NOTA
Quando incappate in problemi di run, prima di imbarcarvi in un
troubleshooting approfondito, il consiglio è di disinstallare l’app sul
device e procedere a un flutter clean e successivamente a un flutter
pub get per per ottenere nuovamente le dipendenze dopo aver fatto
pulizia delle build.

Profile
La modalità profile consente di accedere a una serie di
dettagli legati alle performance dell’applicativo. L’app buildata
con flutter run --profile sostanzialmente esegue una build
“release” con il supporto dei devTools esclusivamente nel
particolare delle performance.
Una build profile è in genere usata per indagare o riscontrare
se sono presenti problemi di performance. È importante che la
build sia eseguita su un device fisico e che sia un device di
fascia media, proprio per cercare di rispecchiare il target
comune.
In Figura 12.8, eseguiamo una build profile in un simulatore,
solo per semplicità. È possibile vedere le informazioni che si
possono ricavare dai DevTools; inoltre, abilitando Performance
Overlay è possibile visionare la fase di rendering direttamente
sul device.
Il grafico presente direttamente sull’applicazione mostra in
realtime il carico della GPU e della UI: nel primo caso si può
evincere il carico di lavoro causato dalla fase di painting e
rendering, nel secondo tutto il restante codice applicativo.
In tempo reale, muovendosi tra le varie schermate dell’app
sarà possibile notare eventuali picchi che ci indirizzeranno sulla
giusta strada. In genere, un problema di performance si
presenta in situazioni in cui i frame impiegano più di 16ms per
essere renderizzati.

Figura 12.8 Tab Performance.

Esercizi per casa


Durante la spiegazione del codice abbiamo già introdotto
qualche consiglio e qualche miglioria da poter applicare.
Rispetto a quanto già detto, un ulteriore passo avanti potrebbe
essere usare le api per catturare l’indirizzo oltre che la posizione
GPS, sicuramente una feature interessante da implementare. Il
package che potrebbe fare al caso vostro è:
https://pub.dev/packages/geocoding. Geocoding offre una API per

trasformare appunto le coordinate GPS in quello che in inglese


è chiamato human readable address. Inoltre, a seconda del
device utilizzato, il layout potrebbe non mantenere “ordinato” il
testo, in quanto non è pensato per essere “particolarmente”
responsivo. Potrebbe essere interessante approcciare i widget
che consentono di realizzare un layout responsive. Scommetto
che qualche capitolo più avanti il tema verrà approfondito.
Capitolo 13

Test

Lo sviluppo della fase di test dovrebbe avere la stessa


rilevanza dello sviluppo dell’applicativo: questo è il mantra. Per
sviluppare codice efficiente e manutenibile, è importante che lo
sviluppo dei test non venga relegato, come spesso accade, alla
fase finale del ciclo di sviluppo, ma che accompagni
quest’ultimo lungo tutto il suo percorso.
Sviluppata una classe, sviluppato un widget, sviluppata una
funzionalità, è indispensabile che vengano sviluppati un set di
test per scongiurare regressioni oltre a verificare puntualmente
l’infallibilità del codice.
Rispetto alla quantità di test da predisporre, esiste una
metrica chiamata code coverage, che consente di ottenere un
report dettagliato sull’analisi del codice che viene “validato” dai
test sviluppati. È auspicabile che i test sviluppati coprano più
codice possibile.
NOTA
Esistono diverse metodologie di sviluppo, tra cui TDD (test-driven
development). Il test in modalità TDD è sviluppato prima ancora
dell’applicativo.

Lo sviluppo dei test non è solo indispensabile a scrivere


codice “migliore”, ma consente anche di organizzare il software
all’interno di una pipeline per automatizzare, oltre che i test
stessi, la procedura di build e di release.
NOTA
Il termine pipeline si usa per indicare una serie di tool che lavorano in
sinergia per automatizzare il processo di sviluppo, specificamente per
la fase di build, test e deploy.

Flutter offre diversi strumenti per integrare i test, ma prima di


procedere allo sviluppo è bene capire come gestisca questo
aspetto.
Flutter consente di utilizzare tre tipologie di test:
unit test;
widget test;
integration test.
Gli unit test sono test che mirano a testare singoli elementi.
L’esempio classico di uno unit test prevede il test di una classe,
di un metodo o anche di una funzione. Questo approccio
consente di testare la singola unità e l’obiettivo dovrebbe essere
quello di avere tutti i test necessari a coprire tutto il codice
sviluppato.
Il widget test, come si intuisce dal nome, si concentra sul test
del singolo widget. I test coprono, oltre che logica del widget,
anche la User Interface, ciò significa che è possibile interagire
con la struttura del layout. Un widget test potrebbe simulare
finanche la fase di scrolling per procedere a un inserimento dati.
Infine, i test di integrazione (integration test) hanno una
visione più ampia: può essere testata l’intera app o buona parte
di essa. Il test viene eseguito direttamente sul device o su un
emulatore. Infine, sarà anche possibile accedere alle statistiche
di performance (profiling).
Lavoreremo creando dei test per l’app demo meteo appena
sviluppata.
Il primo passo è quello di installare il package per i test.
$ flutter pub add test --dev

Fatto questo, procediamo.


NOTA
Il parametro dev indica a Flutter di gestire il package come
dev_dependencies, ovvero dipendenze legate ai test e non al
funzionamento dell’app.

Unit Test
L’obiettivo del nostro unit test sarà quello di testare la classe
Weather. I test che predisporremo sono 4:
test del costruttore;
test della factory;
test del metodo convertDirection;
test del metodo iconFromCode.

NOTA
Premessa importante e doverosa. Nel nostro caso lavoreremo
“cablando” (in gergo si dice così) all’interno del test la risposta che ci
aspetteremo dalle API esposte dal nostro provider meteo. Questa
pratica, per quanto sia efficace, andrebbe approcciata con l’uso di un
MockServer. Vediamo di approfondire il tema poco più avanti.

Listato 13.1 weather_test.dart


import 'dart:convert';
import 'package:demo_meteo/models/weather.dart';
import 'package:test/test.dart';

void main() {
test('Weather class initialization', () {
Map t = jsonDecode(
'{"temperature": 11.5,"windspeed": 8.7,"winddirection":
336.0,"weathercode": 0,"time": "2023-01-22T00:00"}');
Weather w = Weather(
time: t['time'],
code: t['weathercode'],
codeIcon: Weather.iconFromCode(t['weathercode']),
temperatureMin: t['temperature_2m_min'],
temperatureMax: t['temperature_2m_max'],
windspeed: t['windspeed'],
winddirection:
Weather.convertDirection(t['winddirection'].toDouble()));

expect(w.windspeed, 8.7);
});

test('Weather class initialization by factory', () {


String t =
'{"temperature": 11.5,"windspeed": 8.7,"winddirection":
336.0,"weathercode": 0,"time": "2023-01-22T00:00"}';
Weather w = Weather.fromJson(jsonDecode(t));

expect(w.code, 0);
});
test('Weather convertDirection method', () {
String w = Weather.convertDirection(337);

expect(w, 'North');
});

test('Weather iconFromCode method', () {


int t = Weather.iconFromCode(48);

expect(t, 0xe820);
});
}

Il file deve essere posizionato all’interno della directory test.


Analizziamo il codice nel dettaglio.
Il primo passo è quello di importare le dipendenze,;nel nostro
caso la dipendenza principale è il progetto demo_meteo che
viene indicato con il suffisso package, fino a raggiungere il path
desiderato. Una volta importate le dipendenze necessarie
strutturiamo i test tramite la funzione test.
La funzione è così definita:
void test(
Object description,
dynamic body(
),
{String? testOn,
Timeout? timeout,
dynamic skip,
dynamic tags,
Map<String, dynamic>? onPlatform,
int? retry}
)

Il parametro description viene internamente convertito in


stringa, pertanto possiamo utilizzarla come tale e descrive
brevemente lo scopo del test. Body è effettivamente la nostra
callback che conterrà il test. TestOn può essere usato per indicare
che il test è specifico per platform, è possibile reperire la lista
qua: https://github.com/dart-lang/test/tree/master/pkgs/test#platform-
selectors. Timeout può essere usato per modificare il timeout di
default per l’esecuzione del test, che corrisponde a 30 secondi;
il timeout indica il fallimento del test. Skip se settato a true indica
a Flutter di non eseguire il test.
Il parametro tags consente di associare dei test a dei tag
specifici; in questo modo è possibile da linea di comando
indicare l’esecuzione di test specifici. Per esempio:
test('Weather iconFromCode method', () {
int t = Weather.iconFromCode(48);

expect(t, 0xe820);
}, tags: 'iconFromCode');

$ flutter test test/weather_test.dart --tags iconFromCode

Aggiungendo il parametro –tags solo il test iconFromCode verrà


eseguito.
Il parametro onPlatform consente di parametrizzare i test per
specifici platform. Per esempio:
}, onPlatform: {
browser: Skip('Browser is currently not supported'')
});

Infine, retry indica quante volte il test deve essere ripetuto per
marcarlo come fallito; di default non è prevista nessuna
ripetizione.
La callback (body) che viene eseguita dalla funzione test è il
cuore del test. Come possiamo vedere, non facciamo altro che
fornire il dato in input necessario al test specifico e verificare
tramite la funzione expect che il dato corrisponda al valore atteso.
Per esempio, nel test del metodo iconFromCode, non facciamo
altro che invocare il metodo passando come parametro il valore
48 e verificando che l’output atteso sia 0xe820. Così facendo,
verifichiamo la logica del metodo e, se questo dovesse
cambiare nel tempo, il nostro test fallirà; questo è classico un
esempio di regressione.
Più approfonditi saranno i test, maggiore sarà la possibilità di
riscontrare un bug nel codice in fase di sviluppo.
Eseguiamo il codice direttamente da VS Code oppure da
console con il comando:
$ flutter test test/weather_test.dart

✓ Weather class initialization


✓ Weather class initialization by factory
✓ Weather convertDirection method
✓ Weather iconFromCode method

00:02 +4: All tests passed!

L’output indica la durata dell’esecuzione del test (2 secondi), il


numero di test effettuati (4) e il loro esito (positivo).
È anche possibile, soprattutto quando si hanno molti test per
singolo file, raggrupparli tramite la funzione group. Per esempio:
group('Weather static methods', () {
test('Weather convertDirection method', () {
String w = Weather.convertDirection(337);

expect(w, 'North');
});

test('Weather iconFromCode method', () {


int t = Weather.iconFromCode(48);

expect(t, 0xe820);
});
});

Da console è possibile eseguire test su gruppi specifici:


$ flutter test test/weather_test.dart --name "Weather static methods"
00:01 +2: All tests passed!

Widget Test
Questa tipologia di test consente di verificare se il widget è
funzionante. A grandi linee, si può riassumere come un test per
verificare se un widget è presente nel relativo albero e che il suo
comportamento e la sua UI rispetti le aspettative.
Esattamente come per gli unit test, i widget test necessitano
di una dipendenza specifica. È necessario verificare la presenza
di questa porzione all’interno del file pubspec.yaml. Questa
dipendenza è comunque presente nel package:test,
precedentemente installato.
dev_dependencies:
flutter_test:
sdk: flutter

Il nostro obiettivo è quello di testare un widget. Procediamo


con uno step piuttosto elementare: creiamo il file home_test.dart
nella directory test.

Listato 13.2 home_test.dart


import 'package:demo_meteo/main.dart';
import 'package:demo_meteo/screens/home_screen.dart';
import 'package:flutter_test/flutter_test.dart';

void main() {
testWidgets('CurrentWeather Widget data loaded', (tester) async {
const widget = MyApp();

await tester.pumpWidget(widget);
expect(find.byType(MyHomePage), findsOneWidget);
});
}
Il test viene definito dalla funzione testWidgets. Il suo costrutto
è:
void testWidgets(
String description,
WidgetTesterCallback callback,
{bool? skip,
Timeout? timeout,
@Deprecated('This parameter has no effect. Use `timeout` instead. '
'This feature was deprecated after v2.6.0-1.0.pre.') Duration?
initialTimeout,
bool semanticsEnabled = true,
TestVariant<Object?> variant = const DefaultTestVariant(),
dynamic tags}
)

void testWidgets(
String description,
WidgetTesterCallback callback,
{bool? skip,
Timeout? timeout,
@Deprecated('This parameter has no effect. Use `timeout` instead. '
'This feature was deprecated after v2.6.0-1.0.pre.') Duration?
initialTimeout,
bool semanticsEnabled = true,
TestVariant<Object?> variant = const DefaultTestVariant(),
dynamic tags}
)

Il parametro description descrive il test. La callback è la


funzione che eseguirà il test. Skip, timeout e tags hanno la
stessa logica vista nel paragrafo precedente. SemanticsEnabled
attiva di default la semantica. Infine, variant consente di
introdurre delle varianti del test che stiamo implementando.
Breve esempio dell’uso di variant.
testWidgets('Color for mobile platform', (tester) async {

final widget = MaterialApp(home: Scaffold(color: colorVariant, body:


CurrentWeatherView(location: p)));
await tester.pumpWidget(widget);
expect(find.byType(MyHomePage), findsOneWidget);
},
variant: colorVariant());

Se immaginiamo colorVariant come una lista di colori,


possiamo, con un solo test, eseguirlo diverse volte con questa
piccola variante. colorVariant corrisponde a una classe definita
nel test che estende TestVariant e che al suo interno, tramite un
iterable, effettuerà il numero di test preconfigurati. L’argomento
può essere approfondito al seguente indirizzo:
https://api.flutter.dev/flutter/flutter_test/testWidgets.html

NOTA
Semantic, che ho tradotto con il concetto di semantica, è un widget
usato per arricchire con informazioni i widget che compongono
l’albero. Questo consente la lettura dell’applicazione a strumenti terzi,
per esempio tools per l’accessibilità.

Una volta definita la descrizione del test, passiamo alla


callback. Nel nostro esempio istanziamo la classe MyApp, che
altro non è che il widget che vogliamo testare. Dopo aver
inizializzato la classe è necessario “avviare” il widget, e questo
viene fatto con pumpWidget. Questo metodo può essere
immaginato come il corrispettivo di runApp.
La logica che dobbiamo aspettarci è quella di un rendering
“statico”; ciò che intendo dire è che pumpWidget si fermerà al primo
frame che verrà renderizzato. Nel nostro caso questo aspetto è
soddisfacente, in quanto tramite il metodo expect andiamo a
cercare nell’albero se è presente il widget MyHomePage. La
ricerca è effettuata tramite find.byType e il parametro
findsOneWidget indica che cerchiamo effettivamente un unico
widget. Oltre a find.byType, esistono altre varianti che ci aiutano
nella ricerca dei widget, per esempio: find.byText. Rispetto alla
ricerca tramite tipo, byText consente di cercare un widget di tipo
Text tramite una stringa specifica.
Infine, oltre a findsOneWidget, possiamo usare: findsWidgets,
findsNWidgets e findsAtLeastNWidgets. Il primo consente di catturare
più di un widget, il secondo di definire un numero preciso di
widget da ricercare e l’ultimo almeno un numero preventivato di
widget.
Come nel caso dell’unit test precedente, in Flutter è possibile
eseguire il widget test, con il comando flutter test ... In questo
caso eseguiamo il test home_test.dart tramite la tab Test di VS
Code, gestibile con il comodo pulsante di run.

Figura 13.1 testWidgets in VS Code.

Mock
Il successivo widget da testare è CurrentWeatherView.
Questo widget introduce una complessità maggiore, in quanto il
widget in questione necessita di reperire i dati tramite una
chiamata http. Questo rappresenta una attività mandatoria per il
nostro test e abbiamo necessità di risolverlo con un nuovo
approccio in quanto Flutter non consente di eseguire realmente
chiamate http durante i test.
Ogni chiamata http effettuata in fase di test viene intercettata
dal framework e fornisce un falso http code 400 (Bad Request),
come se la chiamata non avesse avuto successo. Il perché non
sia possibile effettuare chiamate http “reali” durante la fase di
test è condivisibile. Introdurre una chiamata di rete all’interno di
un test introduce possibilità di errori non previsti, soprattutto in
queste tipologie di test che dovrebbero ragionare a scatola
chiusa.
Detto ciò, abbiamo due possibilità: una buona e una cattiva.
Quella buona ragiona sul fatto che una dependency injection
che si rispetti dovrebbe farci ragionare e scrivere codice
migliore; non è questo il caso, noi siamo più pragmatici.
Il widget CurrentWeatherView non implementa la logica di
business all’interno del suo widget, ma lo fa tramite una
funzione esterna che non ha nessuna dipendenza da
CurrentWeatherView. Questa implementazione non è del tutto
corretta, ma aspettavo questo momento per confessarlo.
Una corretta implementazione sarebbe stata quella di rendere
argomento delle funzioni fetchCurrentWeather e fetchForecastWeather
il client http. Così facendo, avremmo la possibilità, all’interno del
nostro test, di usare un client http fake come oggetto argomento
delle funzioni, e in questo caso avremmo parlato di dependency
injection.
Il concetto di client http fake può essere tradotto come mock.
Di fatto, un mock è un oggetto che simula il comportamento di
un altro oggetto, riuscendo a testare il codice senza poter
accedere alla risorsa originale.Per esempio:

Future<Weather>
fetchCurrentWeather(
{
required double latitude,
required double longitude,
required http.Client client
}
) async {
final response = await client.get(Uri.parse('https://api.open-
meteo.com/v1/forecast?
latitude=$latitude&longitude=$longitude&current_weather=true'));

Rispetto a quanto fatto, non solo noi non abbiamo gestito il
client http come dipendenza, ma abbiamo usato il package:http
e il metodo statico get. Per l’uso del mock è necessario
modificare leggermente l’implementazione, sostituendo l’uso di
http.get con http.Client. Questo consente di passare l’istanza

della classe e di conseguenza rende più semplice creare il


mock, ovvero fare l’override dei metodi necessari (get).

Mockito
Se volessimo portare avanti questa strada, il tool che fa al
nostro caso è mockito (https://pub.dev/packages/mockito). Creiamo
un piccolo esempio.
@GenerateMocks([http.Client])
void main() {
testWidgets('....', (WidgetTester tester) async {
final client = MockClient();
when(client.get(url)).thenAnswer(
(value) async {
return http.Response('response..', 200);
},
);
});
}

Con @GenerateMocks creiamo il mock di http.Client; in questo


modo viene creata la classe MockClient. Nel test è necessario
istanziarla e tramite when è possibile definire la risposta alla
chiamata che verrà fatta all’url definito nel metodo get. In questo
modo, ogni chiamata a quell’url avrà il suo mock.

Nock
L’alternativa alla precedente strada è un pò più sbrigativa: si
tratta di Nock (https://pub.dev/packages/nock). Rispetto a quanto
visto (il connubio tra http e mockito), questa soluzione si basa
su un concetto diverso. Flutter consente, tramite la classe
HttpOverrides, di sovrascrivere la classe HttpClient. Questa, a
differenza del package http, è una classe a basso livello poco
user-friendly, che comunque non è da preferire a http (lo
consiglia anche la documentazione di Flutter), ma ci consente di
eseguire ugualmente chiamate di rete. Procediamo con il test
del widget CurrentWeatherView.

Listato 13.3 current_weather_test.dart


import 'package:flutter_test/flutter_test.dart';
import 'package:nock/nock.dart';

import 'package:flutter/material.dart';
import 'package:demo_meteo/screens/current_screen.dart';
import 'package:geolocator/geolocator.dart';

void main() {
setUpAll(nock.init);

setUp(() {
nock.cleanAll();
});

Position p = Position(
longitude: -122.406417,
latitude: 37.785834,
timestamp: DateTime.now(),
accuracy: 10,
altitude: 800,
heading: 1,
speed: 10,
speedAccuracy: 1);

String domain = 'https://api.open-meteo.com';


String url =
'/v1/forecast?
latitude=37.785834&longitude=-122.406417&current_weather=true';
String json =

'{"latitude":37.78929,"longitude":-122.422,"generationtime_ms":0.2579689
025878906,"utc_offset_seconds":0,"timezone":"GMT","timezone_abbreviation
":"GMT","elevation":49.0,"current_weather":
{"temperature":8.8,"windspeed":32.8,"winddirection":15.0,"weathercode":0
,"time":"2023-01-23T11:00"}}';

testWidgets('CurrentWeather Widget CircularProgressIndicator',


(tester) async {
nock(domain).get(url).reply(200, json);

final widget =
MaterialApp(home: Scaffold(body: CurrentWeatherView(location:
p)));

await tester.pumpWidget(widget);
expect(find.byType(CircularProgressIndicator), findsOneWidget);
});

testWidgets('CurrentWeather Widget data loaded', (tester) async {


nock(domain).get(url).reply(200, json);

final widget =
MaterialApp(home: Scaffold(body: CurrentWeatherView(location:
p)));

await tester.pumpWidget(widget);
await tester.pump(const Duration(seconds: 5));
expect(find.text('Temperature: '), findsOneWidget);
});
}

Nell’esempio appena proposto, il primo passo è procedere


alla creazione della struttura di base per l’utilizzo di nock,
tramite l’uso dei metodi Init e cleanAll. In questo caso, non è
necessario creare un mock e non dovremmo cambiare il codice
delle funzioni che possono continuare a utilizzare il metodo
http.get.

Un altro passo mandatorio è quello di creare una falsa


posizione, poiché è necessario all’esecuzione del widget.
Abbiamo diversi modi per farlo, istanziare la classe Position è
uno di questi. Predisposti tutti i fields necessari, possiamo
procedere con il test.
Il primo test verifica se esista il widget
CircularProgressIndicator ed è necessario implementare anche
MaterialApp, in quanto ci sono dipendenze necessarie per la
build del widget.
Il secondo test fa uso del metodo pump, che invita Flutter a
renderizzare il nuovo frame, quello successivo al loader e che
viene aggiornato da FutureBuilder, il quale scatena un nuovo
build e di conseguenza un nuovo rendering del widget.
L’evento che procede al prossimo frame è gestito da pump e
con la property duration possiamo anche definire quanto tempo
attendere per il rendering. Infine, find.text consente di cercare la
stringa ‘Temperature:’ che conferma il corretto rendering del widget
CurrentWeatherView con i “finti” dati ottenuti dal mock. Nel caso
in cui, per esempio, ci trovassimo di fronte a un’animazione con
diversi frame, pumpAndSettle invocherebbe n volte il metodo pump
fino all’attesa dell’ultimo frame.
Le potenzialità offerte WidgetTester sono innumerevoli; si
può, per esempio, verificare se un widget è visibile nella
porzione di scroll in cui ci troviamo tramite il metodo
ensureVisible, è possibile inserire dei dati con enterText, “tappare”

un widget tramite il metodo tap o simulare una gesture tramite


startGesture. Insomma, Flutter fornisce tutti gli strumenti per
testare i widget.

Integration Test
I test precedenti miravano a testare porzioni di codice
sorgente, i test di integrazione hanno l’obiettivo di testare l’intera
applicazione.
Per gestire i test di integrazione è necessario modificare il file
pubspec.yaml, aggiungendo la chiave integration_test.

dev_dependencies:
flutter_test:
sdk: flutter
integration_test:
sdk: flutter
Procediamo creando la directory integration_test dentro la
nostra root e al suo interno posizioniamo il file app_test.dart

Listato 13.4 app_test.dart


import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import 'package:demo_meteo/main.dart' as app;

void main() {

testWidgets('first integration test example', (tester) async {


app.main();
await tester.pump();
expect(find.text('waiting location..'), findsOneWidget);
});
}

In questo esempio abbiamo inizialmente definito la libreria


main.dart come “app”, per poter accedere alla sua funzione main.

È necessario utilizzare questo “casting” poichè abbiamo due


funzioni main nello stesso codice: il primo appartiene ad
app_test.dart e il secondo a demo_meteo/main.dart. L’uso della

keywork as nell’import evita questa collisione.


Dopo l’import, il secondo passo è definire il nostro test con la
solita funzione testWidgets. Al suo interno, richiamiamo la
funzione main di demo_meteo/main.dart, per avviare l’app. Sappiamo
che il metodo pump è necessario per renderizzare il primo frame;
infine, cerchiamo la stringa prodotta con il widget Text all’interno
del nostro albero.
Eseguiamo il comando:
$ flutter test integration_test
00:08 +0: loading /Users/vgiacchina/Library/Mobile
Documents/com~apple~CloudDocs/work/demo_meteo/integration_test/app_test.
dart Ru00:27 +0: loading
/Users/vgiacchina/Library/Mobile
Documents/com~apple~CloudDocs/work/demo_meteo/integration_test/app_test.
dart
00:30 +0: loading /Users/vgiacchina/Library/Mobile
Documents/com~apple~CloudDocs/work/demo_meteo/integration_test/app_test.
dart 3.5s
Xcode build done. 22.2s
00:35 +1: All tests passed!

Rispetto ai test precedenti, possiamo vedere quanto questo


testo sia diverso dagli altri. Questo test ha invocato il nostro
simulatore, nel mio caso quello che avevo predisposto e
collegato a VS Code (è possibile specificarne altri, leggete l’help
del comandi flutter test), ha renderizzato il frame richiesto e ha
terminato con successo il test.
È possibile integrare con il device, per esempio dare il
consenso alla cattura della posizione del device. Queste
operazioni esulano un po’ dall’argomento specifico Flutter e
dovranno essere gestite direttamente con i tools relativi
all’ambiente di riferimento dell’OS mobile, per esempio adb o
tramite l’utility applesimutils.
Per esempio:
(Android) adb shell pm grant <package> <permission>
(iOS) applesimutils --byName <deviceName> --bundle <bundleId> --
setPermissions location=always

Le potenzialità degli integration test sono veramente infinite,


possiamo applicarvi tutte le conoscenze acquisite nel capitolo.
Il test non è l’unica possibilità offerta del framework; il
package integration_test consente anche la profilazione dell’app.
È meglio che questa sia la build più vicina a quella di release
per avere un dato più utile e coerente possibile.

Profile
Rispetto a quanto visto, è necessario introdurre nel test la
classe IntegrationTestWidgetsFlutterBinding, che ci consente di
intercettare dati di strumentazione nativi prima dell’esecuzione
dell’app. Inoltre, potremo utilizzare l’istanza
IntegrationTestWidgetsFlutterBinding.ensureInitialized() per accedere
al metodo traceAction e profilare le operazioni che possono
essere eseguite nella callback parametro di traceAction.
È possibile utilizzare il set di metodi offerti da WidgetTester,
per esempio simulare le fasi scroll, inserire dati, tappare e
quant’altro. Queste operazioni possono essere profilate e i dati
generati verranno storati tramite la key reportKey.

final binding =
IntegrationTestWidgetsFlutterBinding.ensureInitialized();

testWidgets('failing test example', (tester) async {


app.main();
await tester.pump();

await binding.traceAction(
() async {
await tester....
},
reportKey: 'profile_timeline',
);
});

Una volta ottenuto l’oggetto Timeline, che appunto contiene


tutto il dettaglio legato alle operazioni generate dal test, è
fondamentale convertire questi dati in un json, che possa essere
consumato da applicazioni terze, per esempio Google Chrome
(chrome://tracing).
Il driver per la conversione può essere utilizzato senza
particolari adattamenti:
import 'package:flutter_driver/flutter_driver.dart' as driver;
import 'package:integration_test/integration_test_driver.dart';

Future<void> main() {
return integrationDriver(
responseDataCallback: (data) async {
if (data != null) {
final timeline =
driver.Timeline.fromJson(data['scrolling_timeline']);

final summary = driver.TimelineSummary.summarize(timeline);


await summary.writeTimelineToFile(
'scrolling_timeline',
pretty: true,
includeSummary: true,
);
}
},
);
}

Per procedere al profile, generando un file TimelineSummary, è


necessario procedere con il comando:
flutter drive --driver=timeline.dart --
target=integration_test/app_test.dart –profile

Figura 13.2 Timeline json su chrome://tracing/.

Coverage
Come introdotto in precedenza, Flutter offre la possibilità di
generare un report di coverage. Eseguiamo il seguente
comando:
$ flutter test --coverage
00:03 +9: All tests passed!

Al termine dell’esecuzione dei test, verrà creata la directory


coverage nella root del nostro progetto e al suo interno un file
lcov.info, che a sua volta contiene di fatto l’output in un formato

standard usato per il coverage. Il suddetto file contiene tutte le


informazioni circa l’esecuzione del test e il relativo coverage.
Per quanto questo file sia già usufruibile da applicativi terzi per
l’analisi del codice, per esempio SonarQube, possiamo
trasformare il file prodotto in un template html.
Dopo aver ottenuto il tool genhtml, eseguire il comando:
$ genhtml coverage/lcov.info -o coverage/test.html
Reading data file coverage/lcov.info
Resolved relative source file path "lib/models/weather.dart" with CWD to
"/Users/vgiacchina/Library/Mobile
Documents/com~apple~CloudDocs/work/demo_meteo/lib/models/weather.dart".
Found 8 entries.
Found common filename prefix "/Users/vgiacchina/Library/Mobile
Documents/com~apple~CloudDocs/work/demo_meteo"
Writing .css and .png files.
Generating output.
Processing file lib/main.dart
Processing file lib/icons/my_flutter_app_icons.dart
Processing file lib/models/weather.dart
Processing file lib/screens/home_screen.dart
Processing file lib/screens/forecast_screen.dart
Processing file lib/screens/current_screen.dart
Processing file lib/services/get_position.dart
Processing file lib/services/open_meteo.dart
Writing directory view page.
Overall coverage rate:
lines......: 49.2% (87 of 177 lines)
functions..: no data found

Terminiamo navigando la directory coverage/test.html.

Figura 13.3 Directory.


Figura 13.4 File scansionati.

Figura 13.5 Dettaglio sorgente.


Capitolo 14

Rendering

Abbiamo ormai un buon livello di conoscenza del framework e


ora tocca chiarire alcuni aspetti che ho trascurato favorendo un
approccio più pragmatico. Approfondire le principali logiche che
Flutter mette in campo durante il processo di rendering è
importante per comprendere meglio il funzionamento interno del
framework.

Widget tree
Prendiamo in esempio questa porzione di codice:
Center(
child: Container(
color: Colors.blue,
child: const Text('Hello World', textDirection: TextDirection.ltr),
));

Rispetto a quanto finora appreso, il widget tree sarebbe


composto dallo schema logico in Figura 14.1.
Per confermare la struttura del widget, proviamo a
ispezionare l’applicazione. In Figura 14.2 è possibile vedere il
widget tree in VS Code.
Quanto appare nel widget tree è conforme alle aspettative.
Entrando nel dettaglio del widget tree, ed è possibile farlo
sempre tramite VS Code (tab Widget Details Tree), appaiono
ulteriori informazioni.
Nella realtà, il widget tree appena raffigurato sarà
leggermente diverso, in quanto i widget potrebbero avere delle
logiche interne e, in base a esse, creare ulteriori widget figli.

Figura 14.1 Widget tree.

Figura 14.2 Widget tree in VS Code.

Se qualcuno fosse interessato a capire a fondo la logica e di


conseguenza analizzare il codice di un determinato widget,
questo è possibile consultando il codice sorgente. Ricordiamoci
che Flutter è un framework open source.
Prendendo in esempio Text o Container, possiamo vedere dal
loro sorgente che, nel caso di Text, all’interno del suo build
method, viene ritornato un widget di tipo RichText

(https://github.com/flutter/flutter/blob/master/packages/flutter/lib/sr
c/widgets/text.dart), mentre all’interno del widget Container
(https://github.com/flutter/flutter/blob/master/packages/flutter/lib/sr
c/widgets/container.dart) ci sono delle condizioni per cui possono
essere creati ulteriori widget figli.
Sbirciamo una porzione del codice sorgente del widget
Container:
@override
Widget build(BuildContext context) {
Widget? current = child;
...
current = Align(alignment: alignment!, child: current);
}
...
if (color != null) {
current = ColoredBox(color: color!, child: current);
}
...
if (decoration != null) {
current = DecoratedBox(decoration: decoration!, child: current);
}
...
if (constraints != null) {
current = ConstrainedBox(constraints: constraints!, child:
current);
}
...
return current!;
}

Come si evince dal codice, è possibile, in base ad alcune


condizioni, che vengano creati ulteriori widget, quali per
esempio Align, ColoredBox, DecoratedBox ecc.Nel nostro caso,
avendo usato il parametro color nel costruttore Container, il widget
da noi definito come child verrà a sua volta avvolto da un widget
intermedio: ColoredBox.
In base a quanto detto, in teoria il nostro widget tree dovrebbe
essere composto come raffigurato in Figura 14.3.

Figura 14.3 Widget tree reale.

Ora il widget tree corrisponde esattamente a quanto mostrato


dall’inspector in Figura 14.4.
Chiarito il concetto che riguarda il widget tree, i più attenti
avranno notato che nell’albero compaiono degli altri elementi:
renderObject. A questo punto si rende necessario un ulteriore
approfondimento.
Figura 14.4 Widget details tree da VS Code.

Element tree
Il widget, che abbiamo già spiegato essere immutabile, di
fatto non contiene altro che una serie di informazioni che
vengono fruite dal framework nel processo di rendering. Il
concetto di immutabilità, però, non si sposa bene con
un’interfaccia utente che di fatto muta e rende possibile la
creazione di un’app interattiva.
Per risolvere l’arcano è bene chiarire che l’albero dei widget
non è l’unico aspetto su cui Flutter si basa per la creazione della
nostra UI.
Dietro l’albero dei widget, esiste un altro albero, chiamato
albero degli elementi (Element tree). Per ogni widget che viene
posizionato nell’albero dei widget, Flutter invoca un metodo
chiamato createElement che crea un corrispettivo al widget
nell’albero degli elementi, chiamato Element.
Un Element ha lo scopo di mantenere un riferimento al widget
tree e si occupa di fare da tramite per la fase di rendering.
Possiamo immaginarlo come un layer intermedio che ha la
finalità ultima di rendere il più performante possibile la fase di
rendering.
Pensiamo quante volte nel corso dei nostri esperimenti
abbiamo invocato il metodo build di un widget, ne abbiamo
creati di nuovi, rimossi e ricreati nuovamente, tutte operazioni
che fanno parte del ciclo di vita dell’applicativo e potrebbero
avere un impatto notevole sulle sue performance. Questo
processo è reso performante da Flutter proprio perché quanto
noi facciamo con l’uso dei widget non ha un reale impatto sulla
fase di rendering, ma è il componente Element a gestire per
conto nostro una serie di ottimizzazioni che consentono un
riutilizzo massivo dei widget.
Torniamo momentaneamente al nostro widget tree. Vediamo
nel dettaglio i nostri widget e la loro gerarchia.
Center:Object->DiagnosticableTree->Widget->RenderObjectWidget-
>SingleChildRenderObjectWidget->Align->Center
Container: Object->DiagnosticableTree->Widget->StatelessWidget-
>Container

ColoredBox:Object->DiagnosticableTree->Widget->RenderObjectWidget-
>SingleChildRenderObjectWidget->ColoredBox

Text:Object->DiagnosticableTree->Widget->StatelessWidget->Text

RichText:Object->DiagnosticableTree->Widget->RenderObjectWidget-
>MultiChildRenderObjectWidget->RichText

Analizzando la gerarchia dei widget, possiamo notare una


differenza importante, per esempio tra i widget di tipo
RenderObjectWidget piuttosto che DiagnosticableTree.

La Figura 14.4 mostra che i widget di tipo non


RenderObjectWidget non verranno renderizzati. Container e Text,

come mostrato in figura, non contengono l’oggetto


RenderObject, ma piuttosto ospitano widget figli.
Tutti i widget presenti nel widget tree si preoccupano di
invocare il metodo createElement. Se spulciassimo le classi
SingleChildRenderObjectWidget e MultiChildRenderObjectWidget (ereditate
da ColoredBox e RichText), troveremmo definito il metodo
createElement, invocato dal framework stesso per generare

l’albero degli elementi. Analizzare il sorgente del framework ci


aiuta a comprendere sempre più a fondo il meccanismo.
Proseguiamo senza indugi.
NOTA
SingleChildRenderObjectWidget e MultiChildRenderObjectWidget sono classi
che differiscono per l’utilizzo di widget che hanno rispettivamente uno
o più di un figlio.

Listato 14.1 basic.dart


class ColoredBox extends SingleChildRenderObjectWidget {

abstract class SingleChildRenderObjectWidget extends RenderObjectWidget


{
const SingleChildRenderObjectWidget({ super.key, this.child });
final Widget? child;
@override
SingleChildRenderObjectElement createElement() =>
SingleChildRenderObjectElement(this);
}

Quando un widget ColoredBox viene istanziato, viene creato


un nuovo Element. Troviamo la definizione della classe
SingleChildRenderObjectElement nel file framework.dart.

Listato 14.2 framework.dart


class SingleChildRenderObjectElement extends RenderObjectElement {
/// Creates an element that uses the given widget as its configuration.
SingleChildRenderObjectElement(SingleChildRenderObjectWidget
super.widget);

Element? _child;

@override
void visitChildren(ElementVisitor visitor) {
if (_child != null) {
visitor(_child!);
}
}

@override
void forgetChild(Element child) {
assert(child == _child);
_child = null;
super.forgetChild(child);
}

@override
void mount(Element? parent, Object? newSlot) {
super.mount(parent, newSlot);
_child = updateChild(_child, (widget as
SingleChildRenderObjectWidget).child, null);
}

@override
void update(SingleChildRenderObjectWidget newWidget) {
super.update(newWidget);
assert(widget == newWidget);
_child = updateChild(_child, (widget as
SingleChildRenderObjectWidget).child, null);
}

@override
void insertRenderObjectChild(RenderObject child, Object? slot) {
final RenderObjectWithChildMixin<RenderObject> renderObject =
this.renderObject as RenderObjectWithChildMixin<RenderObject>;
assert(slot == null);
assert(renderObject.debugValidateChild(child));
renderObject.child = child;
assert(renderObject == this.renderObject);
}

@override
void moveRenderObjectChild(RenderObject child, Object? oldSlot, Object?
newSlot) {
assert(false);
}

@override
void removeRenderObjectChild(RenderObject child, Object? slot) {
final RenderObjectWithChildMixin<RenderObject> renderObject =
this.renderObject as RenderObjectWithChildMixin<RenderObject>;
assert(slot == null);
assert(renderObject.child == child);
renderObject.child = null;
assert(renderObject == this.renderObject);
}
}

Il sorgente appena mostrato è il cuore dell’Element. La classe


SingleChildRenderObjectElement viene invocata dal framework
in fase di creazione del widget. Al costruttore arriveranno le
specifiche di configurazione, il colore nel caso del widget
ColoredBox.
La logica prosegue con il framework che invoca il metodo
mount della classe madre RenderObjectElement per inserire

l’elemento nell’albero degli elementi.

Listato 14.3 framework.dart


@override
void mount(Element? parent, Object? newSlot) {
super.mount(parent, newSlot);
assert(() {
_debugDoingBuild = true;
return true;
}());
_renderObject = (widget as
RenderObjectWidget).createRenderObject(this);
assert(!_renderObject!.debugDisposed!);
assert(() {
_debugDoingBuild = false;
return true;
}());
assert(() {
_debugUpdateRenderObjectOwner();
return true;
}());
assert(_slot == newSlot);
attachRenderObject(newSlot);
super.performRebuild(); // clears the "dirty" flag
}

Soffermiamoci su questa riga, eseguita proprio in fase di


mount.
_renderObject = (widget as
RenderObjectWidget).createRenderObject(this);

La riga in questione indica che alla creazione dell’elemento


viene creato un nuovo oggetto: RenderObject. Quest’ultimo
popola un nuovo albero, chiamato RenderObject tree. A questo
punto abbiamo chiarito che il processo di rendering è scandito
da tre tipologie differenti di alberi.
Il widget tree contiene le specifiche del widget, l’Element tree
mantiene i riferimenti tra il widget tree e il RenderObject Tree,
oltre a rappresentare gerarchicamente gli elementi che
compongono la nostra UI, e infine il RenderObject si occupa
della fase di “painting” degli elementi della UI. Questa logica
consente a Flutter di ragionare e ottimizzare tutti i processi di
rendering, grazie a un meccanismo interno di riutilizzo dei
widget.
Flutter proverà sempre a sfruttare gli stessi Element presenti
nell’Element tree e di conseguenza anche gli stessi
RenderObject, anche se cambiano i widget che compongono il
widget tree. Infatti, uno dei processi fondamentali che
riguardano la fase di rendering è il metodo canUpdate invocato da
Flutter proprio per lavorare di update sull’Element Tree piuttosto
che distruggere e ricreare elementi. Approfondiremo la fase di
update qualche paragrafo più avanti.
Figura 14.5 Strutture ad albero per la fase di rendering.

In Figura 14.5, possiamo vedere la struttura di una porzione di


albero. Gli Element possono essere di due tipi:
ComponentElement e RenderObjectElement. La differenza
fondamentalmente è che solo RenderObjectElement creerà un
oggetto da renderizzare; l’altro è un contenitore. La stessa
logica contraddistingue anche i RenderObject, che a loro volta
hanno diverse tipologie di classe che identificano caratteristiche
di rendering differenti, come: RenderColoredBox,
RenderOpacity, RenderImage, RenderParagraph, ecc.

RenderObject tree
Come già dovrebbe essere chiaro, l’albero dei RenderObject
è quello che viene usato dal framework per disegnare gli oggetti
sullo schermo. Questi oggetti contengono i dati relativi al layout
degli oggetti stessi, utilizzati nella fase di painting. In Figura
14.4, si evince che è possibile ispezionare il contenuto del
renderObject. Vi invito a farlo.
Prendiamo in esempio il RenderObject relativo al widget
ColoredBox.

Listato 14.4 basic.dart


class ColoredBox extends SingleChildRenderObjectWidget {
/// Creates a widget that paints its area with the specified [Color].
///
/// The [color] parameter must not be null.
const ColoredBox({required this.color, super.child, super.key})
: assert(color != null);

/// The color to paint the background area with.


final Color color;

@override
RenderObject createRenderObject(BuildContext context) {
return _RenderColoredBox(color: color);
}

@override
void updateRenderObject(BuildContext context, RenderObject
renderObject) {
(renderObject as _RenderColoredBox).color = color;
}

@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties.add(DiagnosticsProperty<Color>('color', color));
}
}

class _RenderColoredBox extends RenderProxyBoxWithHitTestBehavior {


_RenderColoredBox({required Color color})
: _color = color,
super(behavior: HitTestBehavior.opaque);

/// The fill color for this render object.


///
/// This parameter must not be null.
Color get color => _color;
Color _color;
set color(Color value) {
assert(value != null);
if (value == _color) {
return;
}
_color = value;
markNeedsPaint();
}

@override
void paint(PaintingContext context, Offset offset) {
// It's tempting to want to optimize out this `drawRect()` call if
the
// color is transparent (alpha==0), but doing so would be incorrect.
See
// https://github.com/flutter/flutter/pull/72526#issuecomment-
749185938 for
// a good description of why.
if (size > Size.zero) {
context.canvas.drawRect(offset & size, Paint()..color = color);
}
if (child != null) {
context.paintChild(child!, offset);
}
}
}

È interessante vedere quando sia di facile lettura la fase di


rendering del widget ColoredBox. La classe ColoredBox viene
invocata dall’Element di riferimento, durante la fase di rendering,
per creare o aggiornare _RenderColoredBox, ovvero l’oggetto
disegnato sullo screen. Il seguente oggetto ha il metodo paint
che viene usato per disegnare sulla canvas. Inoltre, nel caso in
cui il colore cambi, viene invocato il metodo markNeedsPaint, che
indica al framework che è necessario ridisegnare l’oggetto, con
la specificità che il repainting non avrà impatto sul layout.
Questo perché, cambiando solo il colore e non, per esempio,
una posizione o una grandezza, non genera impatti su
dimensioni e posizioni di altri elementi.
NOTA
Flutter si basa sul motore grafico chiamato skia e la fase di rendering
si basa su un sistema canvas OpenGL. Flutter disegna sulla canvas
tramite l’uso di coordinate x,y.

Abbiamo preso in esempio un widget che ha un renderObject


molto semplice; il lifecycle si complica ulteriormente per widget
che hanno un impatto sul layout più pesante rispetto a questo
esempio. Il mio interesse era comunque mostrare una
panoramica sul funzionamento della fase di rendering. Un passo
successivo potrebbe essere quello di cimentarsi nella creazione
di un RenderObject custom: sicuramente entrereste ancora più
a fondo nel comprendere il funzionamento del framework.
Per chiudere il cerchio, nell’ispector, quando esplodiamo il
renderObject, notiamo effettivamente tutti i dati ereditati dal
genitore che definiscono le proprietà necessarie al rendering.
Offset, size, vincoli: queste informazioni vengono date in pasto a
RenderObject per il corretto processo di painting.

Keys
Quanto appena spiegato apre conoscenze approfondite sul
framework e abbiamo compreso quanto e come sia stato
strutturato per avere performance ottimali. Ora è il momento di
approfondire un altro tema: le chiavi. Nei capitoli precedenti
abbiamo incontrato più volte il costruttore di un widget che viene
definito con una chiave. Questo passaggio non è mai stato
affrontato nel dettaglio, ma calza perfettamente con il tema che
stiamo affrontando.
Partiamo da questo.
Listato 14.5 main.dart
import 'package:flutter/material.dart';
import 'dart:math';

void main() => runApp(const MaterialApp(home: ExampleWidget()));

class ExampleWidget extends StatefulWidget {


const ExampleWidget({super.key});

@override
State<StatefulWidget> createState() => ExampleWidgetState();
}

class ExampleWidgetState extends State<ExampleWidget> {


List<Widget> tiles = [
TestStatefulWidget(),
TestStatefulWidget(),
TestStatelessWidget(),
TestStatelessWidget(),
];

@override
Widget build(BuildContext context) {
return Scaffold(
body: Row(children: tiles),
floatingActionButton: FloatingActionButton(
onPressed: changePosition, child: const
Icon(Icons.change_circle)),
);
}

changePosition() {
setState(() {
tiles.insert(1, tiles.removeAt(0));
tiles.insert(2, tiles.removeAt(3));
});
}
}

class TestStatefulWidget extends StatefulWidget {


@override
State<TestStatefulWidget> createState() => _TestStatefulWidgetState();
}

class _TestStatefulWidgetState extends State<TestStatefulWidget> {


int number = Random().nextInt(100);

@override
Widget build(BuildContext context) {
return Container(
height: 100,
width: 100,
color: Colors.green,
child: Text('Stateful: $number'));
}
}

class TestStatelessWidget extends StatelessWidget {


final int number = Random().nextInt(100);

@override
Widget build(BuildContext context) {
return Container(
height: 100,
width: 100,
color: Colors.red,
child: Text('Stateless: $number'));
}
}

L’applicazione è piuttosto semplice. Abbiamo una Row con 4


figli. Tutti e 4 vengono istanziati con un valore numerico definito
da number. Questo valore nel corso del tempo non cambia, in
quanto i widget verranno istanziati chiaramente una singola
volta.
In Figura 14.6 possiamo vedere la struttura ad albero dei
widget e la struttura degli elementi.

Figura 14.6 Widget ed Element Tree.

Possiamo evincere chiaramente il rapporto 1:1 rispetto al


widget e il suo elemento. Nel nostro esempio facciamo uso di
una funzione che consente di invertire il primo e il secondo
widget, e contestualmente invertire anche il secondo con il
terzo. Le due coppie di widget (primo e secondo e terzo quarto)
sono effettivamente due tipologie di widget differenti. Una
coppia è un stateful widget, l’altra è uno stateless widget.
Avviata la nostra applicazione, notiamo, in Figura 14.7, lo
stato iniziale.

Figura 14.7 Home main.dart

La voglia di premere il FloatingActionButton è tanta, ed è


giusto lasciarsi tentare qualche volta. Il bottone ha come
onPressed la callback changePosition, che effettuerà lo scambio
dei widget.

Figura 14.8 Post changePosition.

Si evince, dalla Figura 14.8, che cosa sia accaduto dopo


l’evento scatenato da changePosition. Al termine del processo di
build, scatenato da setState, troveremo i widget Stateless che
avranno scambiato sullo screen la loro posizione, mentre i
widget Stateful non avranno subito nessun cambiamento,
quanto meno non dal punto di vista dell’element tree.
Guardiamo la Figura 14.9.

Figura 14.9 Widget ed Element tree post change.

La situazione rappresentata in Figura 14.9 rispecchia quanto


mostrato in Figura 14.8. L’albero dei widget ha subito una
variazione, ma l’albero degli elementi solo in parte. I due widget
stateful, rispetto a quelli stateless, non sono stati aggiornati, e
questo lo si evince da ciò che vediamo sullo schermo.
Per capire che cosa sia accaduto, dobbiamo comprendere i
passaggi che sono stati eseguiti dal framework. Quando un
widget subisce un cambiamento, a grandi linee, il framework
procede con un update, confrontando il “nuovo widget” con
l’Element che ha il “vecchio widget” come riferimento. Questo
passaggio si basa sul metodo canUpdate, e il confronto tra i widget
viene eseguito per tipologia e per chiave.
Listato 14.6 framework.dart
static bool canUpdate(Widget oldWidget, Widget newWidget) {
return oldWidget.runtimeType == newWidget.runtimeType
&& oldWidget.key == newWidget.key;
}

Se i widget soddisfano il confronto per tipo e chiave, il


processo prosegue per il restante albero, cercando così di
procedere all’aggiornamento.
Nel caso del widget TestStatelessWidget, il cambio avviene
poiché il runtimeType è lo stesso (TestStatelessWidget ==
TestStatelessWidget) e lo stesso avviene per la key, che in
questo caso sarà null. Visto che il confronto è positivo, i widget
vengono aggiornati, poiché il widget da aggiornare nell’element
tree corrisponde a quello del widget tree.
Cosa ben diversa per i widget TestStatefulWidget: il processo
è pressoché identico, con la differenza che il dato relativo a ciò
che effettivamente percepiamo differente ($number) è contenuto
nello state. In questo processo il concetto di state viene del tutto
ignorato poiché il confronto ha esito positivo e ciò porta ad
aggiornare il riferimento del widget, ma mantiene comunque un
riferimento al vecchio stato. In poche parole, l’update del
riferimento tra widget tree ed element tree avverrebbe, ma non
tra l’elemento e lo state.
Modifichiamo il sorgente per definire una key nei widget
Stateful.
List<Widget> tiles = [
TestStatefulWidget(key: UniqueKey()),
TestStatefulWidget(key: UniqueKey()),

class TestStatefulWidget extends StatefulWidget {
const TestStatefulWidget({super.key});

@override
State<TestStatefulWidget> createState() => _TestStatefulWidgetState();
}
L’inserimento della key farà in modo che, quando Flutter
confronterà widget ed Element, questi non corrisponderanno
(runtimeType && key) in quanto al primo elemento dell’albero
corrisponderà il primo elemento dell’albero degli elementi con
una key differente. In questo caso. Flutter sarà costretto a
disattivare gli Element e cercare nella sua porzione di albero dei
widget se sono presenti child con le relative key. Se questi
vengono trovati, l’albero verrà ricostruito rispettando
l’ordinamento del widget tree e, infine, gli elementi verranno
renderizzati correttamente. A quel punto l’applicazione avrà il
comportamento atteso.

GlobalKey
Il confronto delle chiavi che abbiamo visto poc’anzi avviene
con un limite ben preciso. In Figura 14.9 viene mostrato che la
ricerca della corrispondenza della key tra i due alberi viene fatta
solo allo stesso livello. Se viene creato un nuovo albero, questo
non verrà analizzato.
Figura 14.10 Ricerca key solo allo stesso livello.

Un test efficace è quello di modificare il sorgente


introducendo un nuovo livello tra i due widget Stateful.
List<Widget> tiles = [
Container(
padding: const EdgeInsets.all(8.0),
child: TestStatefulWidget(key: UniqueKey()),
),
Container(
padding: const EdgeInsets.all(8.0),
child: TestStatefulWidget(key: UniqueKey()),
),
TestStatelessWidget(),
TestStatelessWidget(),
];

Ciò che accade lanciando l’applicazione è che non vengono


invertiti i due widget, piuttosto vengono ricreati da 0, ed è
riscontrabile dal fatto che i numeri randomici cambiano, piuttosto
che cambiare di posizione.
Questo accade perché i widget non risultano gli stessi proprio
perché la key è differente, ma allo stesso modo questa non
potrà essere trovata poiché appartiene a un livello differente.
Questo concetto riassume il fatto che la key ha una visibilità
limitata, ma torna utile al nostro esempio per introdurre una key
con una visibilità globale.
Container(
padding: const EdgeInsets.all(8.0),
child: TestStatefulWidget(key: GlobalKey()),
),
Container(
padding: const EdgeInsets.all(8.0),
child: TestStatefulWidget(key: GlobalKey()),
),

Adesso, i widget verranno scambiati correttamente.


Se state pensando che avremmo potuto inserire una key nel
widget Container, sono fiero di voi, in quanto è la deduzione più
logica. Abbiamo approfittato dell’esempio per introdurre anche
le global key. Un classico esempio in cui potreste farne uso è
nel caso dell’utilizzo di un solo widget in due punti diversi
dell’albero, ma che abbiano entrambi la necessità di accedere
allo stesso stato.

RepaintBoundary
Un aspetto importante del funzionamento interno di Flutter è
senz’altro il processo di “painting”, che in linea di massima viene
nascosto del tutto allo sviluppatore. Abbiamo abbondantemente
compreso il processo di building e rebuilding, che comporta in
molti casi anche il processo di painting, ovvero il processo di
disegno vero e proprio che è a carico del framework ed è
implementato nei vari RenderObject.
Per quanto è vero che nella maggior parte dei casi il metodo
build di un widget scatenerebbe un processo di painting, non è
vero al contrario. Quando un’applicazione diventa piuttosto
complessa potremmo incappare in problematiche legate alle
performance o comunque essere più attenti a questa
problematica.
Il processo di painting, come abbiamo detto, può essere non
del tutto controllato e anche indipendente dal processo di
building del widget. Per esempio, il framework potrebbe
invocare il metodo RenderObject.markNeedsPaint di un widget a
causa di un evento non legato a un setState o comunque a un
cambio dati del modello, a causa di un scroll o a un’animazione
che coinvolge un altro widget.
Il widget RepaintBoundary consente di limitare il processo di
pittura dei widget, che a cascata obbligherebbe di ridipingere i
figli.

Listato 14.7 main.dart


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

void main() {
debugRepaintRainbowEnabled = true;
runApp(const MyApp());
}

class MyApp extends StatelessWidget {


const MyApp({super.key});
@override
Widget build(BuildContext context) {
return const MaterialApp(
home: RepaintExample(),
);
}
}

class RepaintExample extends StatelessWidget {


const RepaintExample({super.key});

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Example'),
),
body: ListView(
padding: const EdgeInsets.all(8),
children: <Widget>[
Container(
height: 250,
color: Colors.amber[600],
child: const Center(child: Text('Entry A')),
),
Container(
height: 250,
color: Colors.grey[100],
child: const Center(child: Text('Entry B')),
),
Container(
height: 250,
color: Colors.green[300],
child: const Center(child: Text('Entry C')),
),
Container(
height: 250,
color: Colors.red[300],
child: const Center(child: Text('Entry C')),
),
],
));
}
}

In questo esempio, facciamo uso della variabile


debugRepaintRainbowEnabled, che consente di visualizzare i layer di

pittura dell’applicazione (è anche possibile tramite i DevTools


accendere questa feature nel Widget Inspector). Nel caso in cui
ci rendessimo conto che il repainting avverrebbe anche per
widget per cui non sia necessario, una soluzione potrebbe
essere quella di posizionare il widget in un layer separato, che a
questo punto non subirebbe l’impatto di un markNeedsPaint. Questa
considerazione nasce dal fatto che stiamo sviluppando
un’applicazione estremamente poco performante e tramite i dati
del profiling notiamo degli importanti rallentamenti.
NOTA
Il termine overlaying indica un processo di sovrapposizione. In Flutter il
processo di painting avviene su layer grafici e questi possono essere
sovrapposti e a loro volta composti da widget che, nel caso subiscano
un cambiamento, obbligano altri widget dello stesso layer al processo
di repainting.

Proviamo a utilizzare il widget RepaintBoundary nel primo


Container.
Container(
height: 250,
color: Colors.amber[600],
child:
const Center(child: RepaintBoundary(child: Text('Entry
A'))),
),

In questo caso, isolando il layer dedicato al widget Text, non è


cambiato nulla, poichè comunque il widget in questione dovrà
necessariamente essere ridipinto, trovandosi all’interno di uno
scroll.
Un esempio opposto potrebbe essere quello di posizionare un
bottone che inizialmente condivide lo stesso layer dello scaffold
e verificare che, posizionato in un layer separato, il processo di
repainting questa volta avvenga solo per il bottone. Anche
questo esempio lascia il tempo che trova, ma rende comunque
l’idea sul funzionamento di RepaintBoundary.
class RepaintExample extends StatelessWidget {
const RepaintExample({super.key});

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Example'),
),
body: Container(
height: 250,
color: Colors.amber[600],
child: const Center(child: Text('Entry A')),
),
floatingActionButton:
RepaintBoundary(child: FloatingActionButton(onPressed: ()
{})));
}
}
Vi invito a rimuovere il widget RepaintBoundary per
comprendere quanto una semplice interazione con il bottone di
fatto scateni un repainting di tutto il layer, inoltre senza che
avvenga nessun rebuild in quanto non è presente nessun
setState.

CustomPainter
CustomPainter è un widget che ci consente di disegnare sulla
canvas. Il suo utilizzo si caratterizza da una classe che estende
CustomPainter, e l’omonimo widget disegnerà a video quanto
definito dal metodo paint.
La classe Path consente di tracciare linee, posizionarsi sulla
canvas in un determinato punto, disegnare figure geometriche.
Metodi come addPolygon, addOval, addArc e addRect, offerti dalla
classe Path, consentono di disegnare le forme più disparate.
Facciamo un esempio, in tema Hitchcock, utilizzando un
approccio più grezzo, ovvero indicare dei punti sulla canvas e
tracciare delle linee, che infine vengono “riempite” creando
figure (chiedo umilmente scusa per la scarsa immaginazione).
Listato 14.8 main.dart
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';

void main() {
debugRepaintRainbowEnabled = true;
runApp(const MyApp());
}

class MyApp extends StatelessWidget {


const MyApp({super.key});

@override
Widget build(BuildContext context) {
return const MaterialApp(
title: 'Example',
home: PaintWidget(),
);
}
}

class PaintWidget extends StatelessWidget {


const PaintWidget({super.key});

@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Container(
decoration: BoxDecoration(border: Border.all()),
height: 300,
width: 152,
child: CustomPaint(
painter: PaintingView(),
)),
));
}
}

class PaintingView extends CustomPainter {


@override
void paint(Canvas canvas, Size size) {
size;
Paint paint = Paint();
Paint paintBall = Paint();
paintBall.color = Colors.red;
paint.style = PaintingStyle.stroke;
paint.strokeWidth = 3;

paint.color = Colors.green;

Path path = Path();

path.moveTo(150, 40);
path.lineTo(130, 70);
path.lineTo(120, 70);
path.lineTo(125, 80);
path.lineTo(120, 100);
path.lineTo(150, 90);
path.lineTo(120, 130);
path.lineTo(110, 180);
path.lineTo(120, 210);
path.lineTo(140, 220);
path.lineTo(140, 230);
path.lineTo(140, 290);
path.lineTo(110, 298);

canvas.drawPath(path, paint);

path = Path();
path.moveTo(135, 85);
path.lineTo(128, 90);
canvas.drawPath(path, paint);

path = Path();
path.moveTo(110, 180);
path.lineTo(150, 180);
canvas.drawPath(path, paint);

path = Path();
path.addOval(Rect.fromCircle(center: const Offset(145, 60), radius:
2));

canvas.drawPath(path, paintBall);
}

@override
bool shouldRepaint(covariant CustomPainter oldDelegate) {
return false;
}
}

Il sorgente in questione, di cui provo profonda vergogna,


implementa una classe, figlia di Container, che ospiterà il nostro
“disegno”. La classe PaintingView deve estendere
CustomPainter e fare l’override di due metodi: paint e
shouldRepaint. Il primo metodo è quello invocato da Flutter per la
fase di paint, il secondo viene invocato da Flutter, nel caso di
una nuova istanza di CustomPainter, per capire se il widget
deve essere ridisegnato o meno. Il metodo paint crea due
istanze Paint, una classe che offre tutti gli strumenti necessari
per disegnare sulla canvas. Facciamo uso di queste due istanze
per separare due processi differenti, con due differenti colori.
Infine, il metodo drawPath consente di disegnare sulla canvas. Il
secondo step è l’istanza Path, che consente di muoverci lungo la
canvas: la usiamo inizialmente con il metodo moveTo e lineTo, che
ci offre la possibilità di posizionarsi sulla canvas, e inizia a
tracciare delle linee. Questa logica si applica partendo dalle
coordinate x,y, che corrispondono a top e left.
L’istanza path viene istanziata nuovamente per disegnare dei
cerchi, tramite l’uso del metodo addOval, che a sua volta fa uso
della classe Rect. Quest’ultima consente di disegnare un
rettangolo ma, grazie all’uso del metodo fromCircle, tramite il
parametro radius, possiamo trasformare il rettangolo in un
cerchio. La chiave offset consente di posizionarlo sulle
coordinate stabilite. Anche in questo caso, facciamo uso del
metodo drawPaint per disegnare le nuove figure.
Le coordinate di default fanno riferimento al Container, ma il
parametro size ci consente di accedere alla grandezza dello
screen, utile per fare ragionamenti più ampi.
Sono conscio che è stato un capitolo piuttosto duro, ma
comprendere i meccanismi legati ai vari processi di rendering
senz’altro paga nel lungo periodo.
Figura 14.11 CustomPainter.
Capitolo 15

Layout Design

Finora abbiamo accumulato tante informazioni e con queste


abbiamo creato tante piccole applicazioni e finanche sviluppato
un’app vera e propria. Un aspetto di cui però non ci siamo
occupati ancora è il tema del layout design dal punto di vista del
cross platform.
Un’applicazione, se il design lo prevede, deve poter essere
eseguita in differenti platform e differenti device. Nonostante la
progettazione di un layout che si “adatti” perfettamente a
dispositivi differenti possa risultare complessa, Flutter offre una
serie di strumenti che consentono di approcciare al meglio
questo aspetto. Entriamo nel dettaglio.

Responsive
Le interfacce responsive sono una caratteristica importante di
un applicativo mobile. Per chi non conoscesse questo termine,
con responsive si intende un’interfaccia che si adatta alle
dimensioni dello screen del device. Il design responsive
consente di modellare un’interfaccia “unica” per diverse tipologie
di device. L’esperienza d’uso responsive è, inoltre,
caratterizzata da un adattamento legato all’orientamento del
device, landscape o portrait, ed è in genere quello che un utente
si aspetta. Per esempio, se l’utente sta visionando una photo
gallery in cui le foto sono organizzate in una gridview, questa
dovrebbe configurarsi diversamente al cambiamento
dell’orientamento del device, offrendo all’utente la possibilità di
visionare le foto nel migliore dei modi.
Dal punto di vista prettamente tecnico, Flutter offre diversi
strumenti per creare un layout responsive. Nonostante il fatto
che non esiste un pattern specifico per fare questo, è comunque
possibile realizzarlo in diversi modi.

SafeArea
Il primo widget a cui fare riferimento è senz’altro SafeArea.
Questo widget, a cui abbiamo fatto già riferimento in passato,
può esservi d’aiuto. SafeArea è un widget che consente di
escludere dal layout della nostra applicazione eventuali
sovrapposizioni con il sistema operativo, per esempio eventuali
barre nel top o nel bottom del sistema operativo.
Figura 15.1 SafeArea.

Per evitare quanto rappresentato in Figura 15.1, abbiamo


diverse strade. Se usiamo SafeArea per avvolgere Scaffold,
viene applicato un padding e lo spazio vuoto verrebbe riempito
dal sistema operativo (non è comunque una pratica consigliata).
In iOS e Android viene applicato uno sfondo nero allo spazio
ritagliato dal widget SafeArea, ma per Android questo
comportamento può essere gestito tramite la funzione
SystemUiOverlayStyle.

Nel caso utilizzassimo l’AppBar di Flutter, verrebbe risolta in


automatico la problematica legata all’overlay, almeno nella parte
superiore dello screen. Il problema rimarrebbe a quel punto solo
per la parte inferiore. Esattamente come per l’AppBar,
bottomNavigationBar ci aiuta a risolvere la problematica anche
per la parte inferiore dell’app. Eventualmente non si voglia fare
uso del widget bottomNavigationBar, si può comunque usare
SafeArea all’interno dello Scaffold, e a quel punto il colore del
background dello spazio creato dal padding non creerebbe
discrepanze grafiche.
return const Scaffold(
body: SafeArea(
child:
Align(alignment: Alignment.bottomLeft, child:
Text('Test'))));
}

MediaQuery
Uno dei primi passi, se non il primo, per disegnare
un’interfaccia responsive è senz’altro sapere quali sono le
dimensioni del nostro screen, e di conseguenza anche
conoscere il suo orientamento.
@override
Widget build(BuildContext context) {
MediaQueryData screen = MediaQuery.of(context);
return Scaffold(
body: SafeArea(
child: Center(
child: Text(
textAlign: TextAlign.center,
'height:${screen.size.height} *
width:${screen.size.width} - orientation: ${screen.orientation} -
devicePixelRatio: ${screen.devicePixelRatio}'))));
}
Figura 15.2 MediaQuery (portrait).
Figura 15.3 MediaQuery(landscape).

Il widget appena presentato consente di catturare


informazioni di dettaglio sullo screen size del device (e non
solo). Le informazioni ottenute sono altezza e larghezza dello
screen in pixel. I pixel organizzati in altezza e larghezza, ottenuti
tramite il widget MediaQuery, sono definiti logici. Diversamente,
i pixel fisici corrispondono alla risoluzione reale del device, che
può essere consultata dalle specifiche del device. È possibile
ottenere i pixel fisici moltiplicando quelli logici per il
devicePixelRatio, in quanto il devicePixelRatio rappresenta la
proporzione tra pixel fisici e logici.
Il devicePixelRatio ci consente di lavorare tranquillamente con
la risoluzione “logica” senza doverci preoccupare di quella fisica.
Lavorare con risoluzioni così alte (Iphone 14 Pro ha una
risoluzione di 2556×1179 pixel) in device dalle dimensioni ridotte
sarebbe praticamente impossibile; basti pensare a quanto
sarebbe “strana” la visualizzazione degli elementi sullo screen.
Oltre alle informazioni relative al sizing, MediaQuery ci
consente di catturare anche l’orientamento del device,
fondamentale per attuare le logiche “responsive”.
Per esempio, applichiamo una logica utilizzando MediaQuery.
@override
Widget build(BuildContext context) {
MediaQueryData screen = MediaQuery.of(context);
return Scaffold(
body: SafeArea(
child: GridView.count(
crossAxisCount:
screen.orientation == Orientation.portrait ? 3 : 4,
children: List.generate(30, (index) {
return Center(
child: Card(
child: Image.network(
'https://source.unsplash.com/random/200x200?
sig=${index}')),
);
}))));
}

NOTA
Il sito unsplash.com offre una API per la generazione di immagini
casuali.
Figura 15.4 Orientation(1).
Figura 15.5 Orientation(2).

Come mostrano la Figura 15.4 e la Figura 15.5, grazie


all’ausilio di MediaQuery siamo riusciti a modificare il
comportamento di GridView a seconda dell’orientamento del
device. L’uso della property orientation ci consente di impostare
il numero di elementi sulla griglia tramite una condizione.

OrientationBuilder
Il widget OrientationBuilder può essere usato per creare un
layout che si basa sull’orientation. A differenza dell’esempio in
cui facciamo esclusivamente uso delle MediaQuery, in questo
caso l’orientation non è riferito allo screen del device, ma alle
dimensioni del widget parent. Se OrientationBuilder fosse figlio
di un Container con una larghezza maggiore dell’altezza,
nonostante il device sia orientato in ‘portrait’, l’orientation
ottenuta da OrientationBuilder sarebbe landscape. Viceversa,
se usassimo MediaQuery alle stesse condizioni, otterremmo
l’orientation portrait.
@override
Widget build(BuildContext context) {
return Scaffold(body: OrientationBuilder(builder: (context,
orientation) {
return SafeArea(
child: GridView.count(
crossAxisCount: orientation.name == 'portrait' ? 2 : 4,
children: List.generate(30, (index) {
return Center(
child: Card(
child: Image.network(
'https://source.unsplash.com/random/200x200?
sig=${index}')),
);
})));
}));
}

AspectRatio, FractionallySizedBox
Abbiamo già trattato il widget AspectRatio. Questo widget può
essere usato per realizzare layout responsive proprio perché
non ragiona su dimensioni fisse, ma su proporzioni. La stessa
logica si applica anche per il widget FractionallySizedBox,
introdotto nel capitolo Layout Widgets.
Size size = MediaQuery.of(context).size;
return Scaffold(body:
SafeArea(child: OrientationBuilder(builder: (context,
orientation) {
return AspectRatio(
aspectRatio:
size.aspectRatio * (orientation.name == 'portrait' ? 5 :
2.5),
child: Container(
child: Card(

In questo esempio, l’aspectRatio viene manipolato in base


all’altezza dello screen.

Flexible
Il widget Flexible consente di gestire, con maggiore efficienza
rispetto a quanto già visto, i figli dei widget Row, Column e Flex.
La feature principale di questo widget è la possibilità di
controllare la “flessibilità”, intesa come capacità di estensione,
dei singoli child.
Diamo un’occhiata al costrutto.
const Flexible(
{Key key,
int flex: 1,
FlexFit fit: FlexFit.loose,
@required Widget child}
)

Il parametro flex è un intero che indica un fattore di grandezza


per il figlio. Il parametro fit, invece, è un enum composto da
due valori: loose (valore di default) e tight. Il valore loose indica al
widget di occupare lo spazio minimo libero, invece tight ragiona
all’opposto.
Procediamo con alcuni esempi per comprendere al meglio
l’uso del widget Flexible.
child: Row(
children: <Widget>[
Flexible(
flex: 2,
child: Container(
decoration: BoxDecoration(
border: Border.all(),
), //BoxDecoration
), //Container
), //Flexible
const SizedBox(
width: 12,
), //SizedBox
Flexible(
flex: 1,
child: Container(
decoration: BoxDecoration(
border: Border.all(),
//BoxDecoration
),
), //Container
) //Flexible
], //<Widget>[]
), //Row
Figura 15.6 Flexible.

In questo esempio abbiamo visto come funziona la


proporzione espressa dal parametro flex. Proseguiamo con un
esempio per comprendere meglio il funzionamento del
parametro fit.
children: <Widget>[
Flexible(
flex: 1,
fit: FlexFit.loose,
child: Container(
decoration: BoxDecoration(
border: Border.all(),
),
child: const Text('FlexFit.loose') //BoxDecoration
), //Container
), //Flexible
const SizedBox(
width: 12,
), //SizedBox
Container(
width: 250,
decoration: BoxDecoration(
border: Border.all(),
//BoxDecoration
),
), //Container
], //<Widget>[]
Figura 15.7 FlexFit.loose.

La configurazione FlexFit.tight,a differenza di loose, si


espande per tutto lo spazio possibile.
NOTA
Il widget Expanded è una configurazione del widget Flexible.
Figura 15.8 FlexFit.tight.

Nel caso in cui il container, figlio di Flexible, avesse una


larghezza di 100 pixel, con la configurazione tight questo si
espanderebbe fino al massimo dello spazio consentito. La
stessa logica si avrebbe con la configurazione loose. Nel caso in
cui il container avesse una larghezza nettamente superiore allo
spazio disponibile, questa rimarrebbe comunque limitata allo
spazio disponibile. Questa è una feature di Flexible, a differenza
di quanto sarebbe accaduto, invece, in Figura 15.9.
Figura 15.9 Overflow.

Percentuali
Rispetto a quanto appena detto, il parametro flex può
esprimere un valore tramite una percentuale, come unità di
misura dello spazio occupabile.
I modi per utilizzare questo approccio, che garantisce la
possibilità di disegnare un layout responsive, sono
approssimativamente due (quantomeno utilizzando le strutture
più semplici): Flexible e MediaQuery. Procediamo con un esempio
che le racchiude entrambi e che sia del tutto responsive, in
altezza e larghezza.

Listato 15.1 main.dart


import 'package:flutter/material.dart';

void main() {
runApp(MaterialApp(
home: Scaffold(
appBar: AppBar(
title: const Text('Example'),
), //AppBar
body: Builder(builder: (BuildContext context) {
Size screen = MediaQuery.of(context).size;
return Column(children: [
Flexible(
flex: 60,
child: Row(
children: <Widget>[
Flexible(
flex: 30,
fit: FlexFit.loose,
child: Container(
margin: const EdgeInsets.all(6),
decoration: BoxDecoration(
border: Border.all(),
),
), //Container
), //Flexible
Flexible(
flex: 50,
fit: FlexFit.loose,
child: Container(
margin: const EdgeInsets.all(6),
decoration: BoxDecoration(
border: Border.all(),
),
), //Container
), //Flexible
Flexible(
flex: 20,
fit: FlexFit.loose,
child: Container(
margin: const EdgeInsets.all(6),
decoration: BoxDecoration(
border: Border.all(),
),
), //Container
), //Flexible
], //<Widget>[]
),
), //Flexible
Flexible(
flex: 40,
child: Row(
children: <Widget>[
Container(
width: screen.width * 0.3 - 12,
margin: const EdgeInsets.all(6),
decoration: BoxDecoration(
border: Border.all(),
),
), //Container
Container(
width: screen.width * 0.5 - 12,
margin: const EdgeInsets.all(6),
decoration: BoxDecoration(
border: Border.all(),
),
), //Container
Container(
width: screen.width * 0.2 - 12,
margin: const EdgeInsets.all(6),
decoration: BoxDecoration(
border: Border.all(),
),
), //Container
], //<Widget>[]
)),
]); //Row
//<widget>[]
//Column
}))));
}

In questo esempio l’uso delle “percentuali” è caratterizzato


dall’uso della chiave flex in Flexible. Inoltre, con MediaQuery,
possiamo ricavare le percentuali tramite l’uso di width e height,
moltiplicandolo per 0.*.
L’esempio che fa uso di MediaQuery si preoccupa anche di
sottrarre il padding per non sforare il 100% della dimensione,
poiché il padding, nello specifico caso di MediaQuery, viene
escluso dal conteggio.
In questo esempio, che non fa uso di nessuna dimensione
statica, abbiamo intrecciato Row e Column, per realizzare un
layout del tutto responsive.
Figura 15.10 Layout responsive(1).
Figura 15.11 Layout responsive(2).

LayoutBuilder
Un altro strumento che Flutter offre per la gestione dei layout
è LayoutBuilder. Il widget in questione consente, esattamente
come per la funzione Builder, di creare un albero dei widget e,
allo stesso tempo, fornire i vincoli del genitore.
LayoutBuilder(
builder: (BuildContext context, BoxConstraints constraints) {}

Le constraints si compongono di 4 elementi:


maximum height (maxHeight);
minimum height (minHeight);
maximum width (maxWidth);
minimum width (minWidth).
Per creare un design responsive è importante non solo
costruire lo scheletro, ma anche estendere la logica ai figli, ed è
quello che può essere fatto grazie all’uso di LayoutBuilder. In
questo esempio vediamo come possiamo applicare due logiche
distinte: istanziare due widget differenti a seconda dei vincoli
forniti da LayoutBuilder.
body: Builder(builder: (BuildContext context) {
return SizedBox(
height: MediaQuery.of(context).size.height * 0.4,
child: LayoutBuilder(builder:
(BuildContext context, BoxConstraints constraints) {
bool twoElement = constraints.maxWidth < 600;
if (twoElement) {
return singularPhotoWidget(constraints);
} else {
return doublePhotoWidget(constraints);
}
}));

Adaptive
I widget che consentono di realizzare un applicativo
responsive sono gli stessi che utilizzeremmo per creare un
design adattivo. In questa tipologia di progettazione ciò che fa la
differenza non è, come per l’approccio responsive, modificare il
comportamento dell’applicativo alla conseguente modifica della
risoluzione, ma piuttosto definire a priori un layout, un
comportamento, per una tipologia o categoria di device.
L’approccio adattivo consente di migliorare notevolmente
l’esperienza d’uso quando abbiamo un applicativo che nasce
per essere usato, per esempio, sia su device mobili che
desktop. In una situazione di questo tipo, la densità di spazio a
disposizione, piuttosto che un’interazione che si basa su input
differenti (mouse, keyboard...) o su una gesture, ci
consentirebbe di progettare la nostra UX in modo
completamente differente. Una navigazione basata su
bottomNavigationBar potrebbe essere sostituita su desktop con
NavigationRail. Potrebbero essere introdotti comportamenti
legati all’uso del mouse, come onHover, e anche i testi possono
essere gestiti diversamente: avendo maggiore spazio,
potremmo essere maggiormente “descrittivi”. Insomma, credo
che il concetto sia chiaro.
NOTA
Il widget NavigationRail consente di creare una barra di navigazione
nella parte lateriale dello schermo.

NOTA
onHover è un evento legato al passaggio del mouse su un widget.

Listato 15.2 main.dart


import 'dart:io';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';

void main() {
runApp(const MyApp());
}

class MyApp extends StatelessWidget {


const MyApp({super.key});

@override
Widget build(BuildContext context) {
return const MaterialApp(
debugShowCheckedModeBanner: false,
title: 'Adaptive example',
home: CheckHomeScreen());
}
}

class CheckHomeScreen extends StatelessWidget {


const CheckHomeScreen({Key? key}) : super(key: key);

@override
Widget build(BuildContext context) {
if (kIsWeb || Platform.isMacOS || Platform.isLinux ||
Platform.isWindows) {
return DesktopLayout();
}
return MobileLayout();
}
}

L’esempio in questione potrebbe essere il primo passo di


un’applicazione adattiva, dove, come da prassi, viene
identificata la tipologia di platform che si sta utilizzando per
proiettare l’applicazione verso un widget root di riferimento,
piuttosto che un altro.
In questo caso, la condizione per la scelta di una strada
legata al Desktop piuttosto che un’,altra, si basa sull’uso della
classe Platform. La costante kIsWeb, invece, consente di
verificare che l’applicazione sia eseguita all’interno di un
browser.
NOTA
La gestione degli input in un’applicazione adattiva potrebbe prevedere
anche l’uso della keyboard come interfaccia di UI. Flutter offre una
classe chiamata Focus, che consente di indirizzare l’input da tastiera
verso specifici elementi piuttosto che applicare logiche specifiche.
Questi scenari sono molto comuni in applicativi desktop che ospitano
un numero cospicuo di input fields.

Strutturare un’applicazione adattativa non preclude che


questa possa essere allo stesso tempo responsive; l’aspetto
fondamentale è senz’altro strutturare e progettare l’applicazione,
consentendo un riuso e una condivisione dei widget.
La sfida più ardua in assoluto rimane la fase di progettazione,
nel dettaglio del layout, della user interface e della user
experience, soprattutto in applicazioni cross platform.
Soddisfare tipologie di utenti differenti che si aspettano
comportamenti differenti su un’unica code base è molto
complesso. Flutter offre strumenti potenti, ma la progettazione è
a carico vostro. Dimenticarsi che si è prima di tutto utilizzatori di
applicazioni mobili e desktop è spesso un errore piuttosto
comune e non tenerne conto, in fase di analisi, renderà la vostra
applicazione poco user friendly, con tutte le conseguenze del
caso.
NOTA
Il progetto Flutterfolio (https://www.flutterfolio.com/) è un esempio delle
potenzialità di un progetto adattivo realizzato in Flutter.
Capitolo 16

State Management

Design pattern
I design pattern sono strumenti che consentono di
approcciare problemi comuni nella progettazione del software.
Flutter si basa su Dart e, come sappiamo, Dart è un linguaggio
Object Oriented. I design pattern, nati con queste tipologie di
linguaggi, consentono di migliorare, rendere più efficiente,
leggibile, usabile e manutenibile il codice sorgente.
In Flutter abbiamo affrontato il tema “gestione dello stato”
tramite l’uso di setState.
L’approccio di Flutter usato nello sviluppo del codice è definito
dichiarativo. Quello che in Flutter viene eseguito per creare la UI
è dichiarare un determinato “comportamento”, e questo è
caratterizzato da due componenti: widget e stato. Alla modifica
di quest’ultimo viene immediatamente modificata la UI,
delegando di fatto tutti i passaggi intermedi per raggiungere
l’obiettivo al Framework. Questa tipologia di programmazione
consente di essere molto più veloci rispetto a quella opposta,
conosciuta come “imperativa”.
NOTA
Possiamo immaginare il processo di setState come “funzionale” e
pertanto rientra nella logica della programmazione dichiarativa.
L’approccio imperativo, invece, può essere immaginato come la
definizione di un algoritmo, dove ogni riga di codice è necessaria al
fine di raggiungere l’ obiettivo.

La gestione dello stato è un aspetto fondamentale, in quanto


consente di fatto l’aggiornamento della nostra UI. Il limite
dell’uso di setState, come a oggi lo conosciamo, diventa palese
nel momento in cui l’applicazione assume medie dimensioni.
Delegare l’aggiornamento ai singoli setState spalmati per tutti i
widget stateful non consente di strutturare bene il codice e
soprattutto renderlo efficiente e leggibile. In queste situazioni è
auspicabile centralizzare il più possibile il flusso di update in
unico punto, piuttosto che avere centinaia di setState sparsi per
tutto il codice. Il concetto di design pattern si sposa bene in
questo contesto, in cui un approccio nuovo, rispetto a quanto
visto, ci consente di migliorare il nostro codice. Rimane centrale
l’approccio per condividere informazioni da tra widget. Uno state
management ha sicuramente questo come fine ultimo ed è
possibile utilizzare diverse soluzioni come “state design pattern”.

Callback
Prima di muoverci verso soluzioni più evolute, procediamo
con un esempio piuttosto semplice. Creiamo una piccola
applicazione e optiamo per l’uso di una callback come
strumento di condivisione di dati tra figlio e genitore. È
chiaramente una soluzione veloce e non strutturata, ma
raggiunge il suo scopo.

Listato 16.1 main.dart


import 'package:flutter/material.dart';

void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});

@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: const MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}

class MyHomePage extends StatefulWidget {


const MyHomePage({super.key, required this.title});

final String title;

@override
State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {


int _counter = 0;

void _incrementCounter() {
setState(() {
_counter++;
});
}

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
const Text(
'You have pushed the button this many times:',
),
Text(
'$_counter',
style: Theme.of(context).textTheme.headlineMedium,
),
IncrementCounter(callback: _incrementCounter),
],
),
));
}
}

class IncrementCounter extends StatelessWidget {


final VoidCallback callback;
const IncrementCounter({super.key, required this.callback});

@override
Widget build(BuildContext context) {
return ElevatedButton(onPressed: callback, child: const
Icon(Icons.add));
}
}

In questo esempio, tramite la callback _incrementCounter,


riusciamo a condividere un dato, e soprattutto consentire al
widget figlio di eseguire il setState direttamente dal widget
padre, consentendo così di centralizzare la gestione dello stato.
NOTA
VoidCallback definisce una funzione che non ha parametro e non ha
valori di ritorno.

SharedAppData
ShareAppData è un’altra soluzione per condividere
informazioni lungo i widget che compongono il widget tree. L’uso
del widget SharedAppData consente di definire una chiave a cui
viene associato un valore. I due metodi che stanno alla base del
widget SharedAppData, sono getValue e setValue. Il primo
consente di ottenere il dato dietro la coppia chiave/valore, il
secondo, invece, permette di scrivere il dato e scatena anche un
rebuild; ciò significa che non è necessario un setState().

Listato 16.2 main.dart


import 'package:flutter/material.dart';

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

class MyApp extends StatelessWidget {


const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: const CounterWidget());
}
}

class CounterWidget extends StatefulWidget {


const CounterWidget({super.key});

@override
State<CounterWidget> createState() => _CounterWidgetState();
}

class _CounterWidgetState extends State<CounterWidget> {


int _counter = 0;

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Example'),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
const ShowSharedValue(appDataKey: 'counter'),
const SizedBox(height: 6),
ElevatedButton(
child: const Icon(Icons.add),
onPressed: () {
_counter++;
SharedAppData.setValue<String, int>(context, 'counter',
_counter);
},
),
const SizedBox(height: 6),
ElevatedButton(
child: const Icon(Icons.remove),
onPressed: () {
_counter--;
SharedAppData.setValue<String, int>(context, 'counter',
_counter);
},
),
],
)),
);
}
}

class ShowSharedValue extends StatelessWidget {


const ShowSharedValue({super.key, required this.appDataKey});

final String appDataKey;

@override
Widget build(BuildContext context) {
final int value =
SharedAppData.getValue<String, int>(context, appDataKey, () =>
0);
return Text('$appDataKey: $value');
}
}

Il seguente esempio fa uso della classe ShowSharedValue


per fare uso del metodo getValue di SharedAppData. Il metodo
getValue offre anche la possibilità di eseguire una funzione se la
chiave non è stata ancora creata; in questo caso, il valore per la
chiave sarà 0.
Infine, facciamo uso del metodo SharedAppData.setValue
direttamente dentro ElevatedButton, per incrementare la chiave
counter di _counter.
L’uso della callback o del widget SharedAppData sono due tra
le tante soluzioni che possono essere messe in atto per
condividere dati e stati tra widget. Soluzioni più semplici per
problemi semplici ed altre più complesse per scenari più ostici. Il
widget InheritedWidget, per esempio, consente di creare uno
stato che sia accessibile da tutti gli altri widget e potrebbe
essere usato per gestire strutture dati più complessi.
NOTA
Anche questo widget si basa sul widget InheritedWidget.

InheritedWidget
InheritedWidget nasce con l’esatto scopo di propagare le
informazioni lungo l’albero dei widget. Creiamo un esempio
semplice ma efficace.

Listato 16.3 main.dart


import 'package:flutter/material.dart';

void main() {
runApp(
const MaterialApp(
home: MyApp(),
),
);
}

class MyApp extends StatefulWidget {


const MyApp({super.key});

@override
State<MyApp> createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {


int count = 0;

void incrementer() {
setState(() {
count++;
});
}

@override
Widget build(BuildContext context) {
return CounterState(
count: count,
incrementer: incrementer,
child: Scaffold(
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: const <Widget>[
SizedBox(height: 6),
GetCounter(),
IncrementCounter(),
],
),
)));
}
}
class GetCounter extends StatelessWidget {
const GetCounter({super.key});
@override
Widget build(BuildContext context) {
return Text('counter: ${CounterState.of(context).count}');
}
}

class IncrementCounter extends StatelessWidget {


const IncrementCounter({super.key});
@override
Widget build(BuildContext context) {
return ElevatedButton(
onPressed: CounterState.of(context).incrementer,
child: const Icon(Icons.add));
}
}

class CounterState extends InheritedWidget {


const CounterState({
super.key,
required this.incrementer,
required this.count,
required super.child,
});

final int count;


final Function incrementer;

static of(BuildContext context) {


return context.dependOnInheritedWidgetOfExactType<CounterState>();
}

@override
bool updateShouldNotify(CounterState oldWidget) {
return count != oldWidget.count;
}
}

L’esempio proposto fa uso del widget CounterState, che


estende la classe InheritedWidget. Il widget CounterState
consente di condividere i dati con la porzione di albero
associata al parametro child. I dati vengono definiti in fase di
istanza del widget CounterState nella classe _MyAppState.
I dati dello state _MyAppState saranno accessibili per tutti i
widget figli. L’uso del metodo of consente di accedere alle
properties e il metodo updateShouldNotify è usato per notificare
eventuali widget ereditati che CounterState è cambiato. Il
metodo dependOnInheritedWidgetOfExactType consente di ottenere
l’istanza di CounterState (parent) più vicina. In questo esempio
abbiamo fatto uso di properties puntuale, ma con strutture dati
più complesse avremmo potuto utilizzare un modello dati con
una classe dedicata.

Provider
Provider (https://pub.dev/packages/provider) è uno state
manager. Il widget che sta alla base di Provider è
InheritedWidget. Provider ha esteso la naturale funzionalità di
InheritedWidget rendendolo uno strumento più completo e
versatile, ed è anche lo strumento che prediligo tra le varie
alternative.
La struttura di Provider consente di condividere le
informazioni tra i widget attraverso l’albero, poiché il widget
InheritedWidgets è di fatto il Parent della struttura e i widget che
ne necessitano possono accedere al suo state.
Figura 16.1 Provider/InheritedWidget design.

$ flutter pub add provider

Procediamo modificando l’app di base di Flutter per gestire lo


state manager di default (setState) con Provider.
Listato 16.4 main.dart
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';

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

class MyApp extends StatelessWidget {


const MyApp({super.key});
@override
Widget build(BuildContext context) {
return ChangeNotifierProvider(
create: (context) => CounterProvider(),
child: MaterialApp(
debugShowCheckedModeBanner: false,
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: Consumer<CounterProvider>(
builder: (context, provider, child) {
return Scaffold(
appBar: AppBar(
title: const Text('Example'),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
const Text(
'You have pushed the button this many times:',
),
Text(
'${provider.counter}',
style:
Theme.of(context).textTheme.headlineMedium,
),
],
)),
floatingActionButton: FloatingActionButton(
onPressed: () =>
provider.increment(provider.counter),
tooltip: 'Increment Counter',
child: const Icon(Icons.add),
));
},
),
));
}
}

class CounterProvider extends ChangeNotifier {


int _i = 0;
int get counter => _i;

increment(int c) {
_i = c + 1;
notifyListeners();
}
}

Nel sorgente in questione abbiamo innanzitutto utilizzato il


widget ChangeNotifierProvider, che abilita il Provider.
ChangeNotifierProvider utilizza due parametri: create, che
definisce la classe che estende ChangeNotifier, e child, che di
fatto è la root del widget tree.
ChangeNotified è la classe che stora i dati e con la quale è
possibile interagire per leggere e scrivere i suddetti dati.
All’interno della classe ChangeNotifier deve esistere la funzione
notifierListener, che segnala al Consumer che un dato è
cambiato ed è necessario ri-eseguire una build, esattamente
come accadeva con setState.
Il fruitore della classe ChangeNotified è detto Consumer.
Infatti, ogni porzione di albero che dovrà accedere al Provider
dovrà essere avvolta dalla classe Consumer. La classe
Consumer ha una funzione di builder che definisce tre
parametri: context, provider e child. provider, di fatto, consente di
accedere ai dati contenuti nella classe ChangeNotifier, mentre
child offre la possibilità di definire un widget che non

usufruirebbe dei dati gestiti dal Provider e che potrà essere


utilizzato e quindi renderizzato all’interno della funzione di
builder.
Consumer<CounterProvider>(
builder: (context, provider, child) {
return Column(
children: <Widget>[
...
Container(child: child),
],
);
},
child: const Text('Test'),
),

È importante avvolgere il widget, o comunque la porzione di


albero che effettivamente scatenerebbe la compilazione, non
troppo in alto, proprio per non scatenare rebuild non richiesti
che comporterebbero comunque spreco di risorse.

Provider.of
Rispetto a quanto visto nell’esempio precedente, Provider
offre un’altra alternativa: Provider.of. In verità, più che
un’alternativa Provider.of è una primitiva, visto che Consumer si
basa su questa soluzione.
Rispetto a Consumer, Provider.of offre la possibilità di
accedere al Provider senza necessariamente scatenare un
evento di build. Nonostante ciò, rispetto a Consumer non è
possibile avvolgere una porzione di albero specifica, infatti
Provider.of effettua la build dell’intera sezione.

Modifichiamo l’esempio precedente a favore di Provider.of.



void main() => runApp(ChangeNotifierProvider(
create: (context) => CounterProvider(), child: const MyApp()));

class MyApp extends StatelessWidget {


const MyApp({super.key});
@override
Widget build(BuildContext context) {
int c = Provider.of<CounterProvider>(context, listen: true).counter;
return MaterialApp(
debugShowCheckedModeBanner: false,
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: Scaffold(
appBar: AppBar(
title: const Text('Example'),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
const Text(
'You have pushed the button this many times:',
),
Text(
'$c',
style: Theme.of(context).textTheme.headlineMedium,
),
],
)),
floatingActionButton: FloatingActionButton(
onPressed: () =>
Provider.of<CounterProvider>(context, listen: false)
.increment(c),
tooltip: 'Increment Counter',
child: const Icon(Icons.add),
)),
);
}
}

NOTA
Attenzione al context, che in questo esempio dovrà contenere il
Provider; per questo motivo è stato spostato in cima rispetto
all’esempio iniziale.

Watch
Un’alternativa all’utilizzo della classe Consumer può essere
data dall’uso dei metodi read, watch e select, presenti nel context.
L’utilizzo da favorire è quello che trovate più comodo.
Read: consente di accedere al modello dati
(ChangeNotifier) senza però scatenare processi di build del
widget al cambio del modello.
Watch: consente di monitorare cambiamenti di stato nel
modello e di procedere al build del widget.
Select: consente di usare la logica di watch specificamente
per un elemento del modello.

Listato 16.5 main.dart


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

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

class MyApp extends StatelessWidget {


const MyApp({super.key});
@override
Widget build(BuildContext context) {
return ChangeNotifierProvider(
create: (context) => CounterProvider(),
child: MaterialApp(
debugShowCheckedModeBanner: false,
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: const CounterWidget()));
}
}

class CounterWidget extends StatelessWidget {


const CounterWidget({super.key});
@override
Widget build(BuildContext context) {
CounterProvider counter = context.watch<CounterProvider>();

return Scaffold(
appBar: AppBar(
title: const Text('Example'),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
const Text(
'You have pushed the button this many times:',
),
Text(
'${counter.counter}',
style: Theme.of(context).textTheme.headlineMedium,
),
],
)),
floatingActionButton: FloatingActionButton(
onPressed: () => counter.increment(counter.counter),
tooltip: 'Increment Counter',
child: const Icon(Icons.add),
));
}
}

class CounterProvider extends ChangeNotifier {


int _i = 0;
int get counter => _i;

increment(int c) {
_i = c + 1;
notifyListeners();
}
}

L’esempio in questione risulta essere in linea con i precedenti


esempi. Al cambio del modello CounterProvider, verrà riseguita
la build di CounterWidget.
Esistono diverse alternative a Provider; le principali sono
BLoC e Redux, per le quali spendiamo solo due parole.

BLoC
BLoC (Business Logic Component, https://bloclibrary.dev) è
un design pattern sviluppato per Dart.
BLoC consente di dividere e organizzare più efficientemente
l’architettura dell’applicazione. La base di BLoC è la
programmazione reattiva e questa, come dice il nome stesso, si
basa sul concetto di propagare costantemente ogni
cambiamento relativo ai dati; così facendo, a ogni reazione che
viene scatenata dalla UI corrisponderà un evento che
aggiornerà l’applicazione.
BLoC usa gli stream per gestire gli eventi, e questo approccio
consente di costruire un controller per disaccoppiare la logica
dalla UI, rappresentata dal widget.
La differenza con Provider si basa sul fatto che BLoC si basa
su StreamBuilder, mentre Provider su Consumer. Il primo
rispecchia ogni cambiamento del modello tramite Stream, il
secondo si basa sul metodo notifyListeners per notificare il
Consumer. Inoltre, c’è una differenza architetturale: BLoC
consente di dettagliare meglio la divisione tra modello e
visualizzazione, definendo diversi stream per fields, mentre nel
caso di Provider vediamo che ChangeNotifier rispecchia il
modello nella sua interezza.

Redux
Redux è certamente un’alternativa a BLoC. Credo che a oggi
la community Flutter sia più orientata all’uso di BLoC, ma è
altresì vero che lo strumento migliore è quello che si conosce
meglio, e Redux è molto popolare tra gli sviluppatori React.
Detto ciò, Redux è uno state management, esattamente come
BLoC, ma con una differenza sostanziale.
L’uso di BLoC prevede una struttura di dialogo tra widget e
controller attraverso eventi asincroni bidirezionali, mentre in
Redux la logica è leggermente diversa. Redux si basa su uno
state management centralizzato: questo avviene tramite uno
storage che potrà essere consultato da tutti i widget senza
dover interagire con la struttura ad albero.
Lo scopo di questo capitolo non è assolutamente trattare
questi design pattern; credo che servirebbero tante altre pagine
per farlo in maniera esaustiva. L’obiettivo era presentare le
alternative a quello che rappresenta, dal mio punto di vista, la
prima scelta per un design che consenta un’efficiente gestione
degli stati, ovvero Provider.
Capitolo 17

Approfondimenti

È giunto il momento di tirare le somme.


I libri tecnici sono sempre una spina nel fianco per un autore,
poiché deve sempre lasciarsi alle spalle informazioni non date e
argomenti non trattati. Non si può mai rendere felici tutti e
questa conclusione è anche un mea culpa con il fine ultimo di
trovare perdono consigliando come proseguire e che cosa
approfondire.
Apprendere un linguaggio e un framework da zero non è per
nulla semplice. Le aspettative che si creano nei confronti di libri
tecnici, manuali e tutto ciò che appartiene a questa categoria
sono spesso una lama a doppio taglio. Il lettore, a volte, si
aspetta di trovare una soluzione, ma non dovrebbe essere
questo l’approccio. Sembrerà strano, ma quello che si dovrebbe
cercare è l’opposto: un problema.
L’approccio che di solito seguo è quello di affrontare ogni
passo come fosse una challenge, una sfida, poiché questo porta
il lettore a comprendere il problema e, infine, la soluzione. Non
ci si deve aspettare di trovare tutte le risposte in un libro tecnico;
piuttosto, l’obiettivo è imparare a trovare una soluzione e questo
si basa sul sapere come e dove cercarla. Spero che quanto
appreso in questa lettura vi abbia fatto capire che cosa è Flutter
e vi abbia dato le basi per comprendere che direzione prendere
per affrontare una nuova sfida. Ogni autore vorrebbe che la data
di scadenza non arrivasse mai, che il volume possa essere di
mille pagine, ma purtroppo non è così, e questo è l’ultimo
capitolo. Affrontiamo qualche tema sparso e dirigiamoci verso i
saluti finali.

Cupertino widgets
Purtroppo, per mancanza di tempo e spazio, non sono riuscito
ad affrontare il tema Cupertino. Tutto quello che abbiamo visto,
però, ci consente di approcciare il tema dei widget Cupertino
con le spalle larghe. In chiusura, voglio lasciare una risposta a
un quesito che a qualcuno sarà balenato.
È possibile avere un design dedicato per Android e uno per
iOS?
Certo che sì.
Anche in quest’ultimo capitolo, non mi tiro indietro dal
presentare altro codice.
In questo esempio faremo uso del package universal_platform
(https://pub.dev/packages/universal_platform).
Dopo aver installato il package flutter pub add

universal_platform, procediamo a eseguire questa applicazione.

Listato 17.1 main.dart


import 'package:flutter/material.dart';
import 'package:universal_platform/universal_platform.dart';
import 'package:flutter/cupertino.dart';

void main() {
runApp(const MyApp());
}

class MyApp extends StatelessWidget {


const MyApp({super.key});

@override
Widget build(BuildContext context) {
if (UniversalPlatform.isIOS) {
return cupertinoWidget();
} else {
return materialWidget();
}
}

Widget platformDetection() {
return Center(
child: Text(
"Web: ${UniversalPlatform.isWeb}\n"
"MacOS: ${UniversalPlatform.isMacOS}\n"
"Windows: ${UniversalPlatform.isWindows} \n"
"Linux: ${UniversalPlatform.isLinux} \n"
"Android: ${UniversalPlatform.isAndroid} \n"
"IOS: ${UniversalPlatform.isIOS} \n"
"Fuschia: ${UniversalPlatform.isFuchsia} \n",
),
);
}

Widget materialWidget() {
return MaterialApp(
debugShowCheckedModeBanner: false,
home: Scaffold(
appBar: AppBar(
title: const Text('Example Material'),
),
body: platformDetection(),
));
}

Widget cupertinoWidget() {
return CupertinoApp(
home: CupertinoPageScaffold(
backgroundColor: Colors.grey,
navigationBar: const CupertinoNavigationBar(
middle: Text('Example Cupertino'),
),
child: platformDetection(),
),
debugShowCheckedModeBanner: false);
}
}
Figura 17.1 Android.
Figura 17.2 iOS.

NOTA
Per quanto sia possibile usare Material design su iOS, il contrario è
vero solo in parte. Non è comunque consigliabile usare il design
Cupertino per applicazioni Android perché ci sono problemi legati
all’uso dei font causati dalla licenza Apple. Rimane comunque
possibile avere nella stessa codebase due stili differenti.
Quanto appena visto è solo il primo passo. Chiedo perdono di
non aver introdotto il design Cupertino come si deve, ma credo
che sarete in grado di affrontare anche questa sfida in
autonomia.

Data storage
La necessità di storare le informazioni affinché vivano oltre la
singola esecuzione dell’app è cosa comune. In Flutter ci sono
diverse alternative; alcune di queste sono focalizzate sulla
sicurezza, altre sui dati. La soluzione shared_preferences è quella,
credo, più semplice da applicare per memorizzare dati in
modalità chiave/valore. Inoltre, shared_preferences si basa sull’uso
di NSUserDefaults in iOS e SharedPreferences in Android. È
importante tenere conto che non è consigliato il suo utilizzo per
dati critici.
$ flutter pub add shared_preferences

Listato 17.2 main.dart


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

void main() {
runApp(
const MaterialApp(
home: MyApp(),
),
);
}

class MyApp extends StatefulWidget {


const MyApp({super.key});

@override
State<MyApp> createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {


int count = 0;
@override
void initState() {
super.initState();
getCounter();
}

void getCounter() async {


final prefs = await SharedPreferences.getInstance();
int? count_ = prefs.getInt('counter');
if (count_ != null) {
setState(() {
count = count_;
});
}
}

void incrementer() async {


final prefs = await SharedPreferences.getInstance();
setState(() {
count++;
});
prefs.setInt('counter', count);
}

@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
const SizedBox(height: 6),
Text('counter: $count'),
ElevatedButton(onPressed: incrementer, child: const
Icon(Icons.add)),
],
),
));
}
}

In questo esempio, in cui rimaniamo fedeli al solito scenario


del counter, facciamo uso della classe SharedPreferences e del
metodo getInstance per accedere all’istanza relativa alla nostra
app e ai due metodi getInt e setInt per accedere e scrivere il
valore della key di riferimento.
Per soluzioni più affidabili si può usare un database Sqlite,
tramite l’uso del plugin sqflite (https://pub.dev/packages/sqflite).
Infine, se abbiamo necessità di storare informazioni critiche in
un layer crittografico, è consigliato l’uso di: Flutter_secure_storage.
La soluzione si basa sull’uso del Keychain di iOs e KeyStore di
Android.
Ci sono anche altre soluzioni, ma credo che abbiamo
elencato le più popolari.

Deploy
Un aspetto totalmente ignorato è quello relativo alla
pubblicazione della nostra applicazione negli store. Google Play
Store e Apple Store avrebbero richiesto altri capitoli e purtroppo
il tempo a disposizione non consentiva di approfondire anche
questi aspetti. A onor del vero, forse servirebbe un manuale a
parte per trattare il tema e tutte le sfaccettature che si porta
dietro.
Gli step iniziali per procedere alla pubblicazione di un’app
sugli store principali hanno comunque aspetti in comune tra le
diverse piattaforme. Le app dovranno essere compilate in
modalità release. Le app di release saranno ottimizzate
rimuovendo tutti gli altri aspetti fondamentali per la fase di
sviluppo, ma non necessari per la release, per esempio gli
strumenti di debug.
I passi per questa fase sono differenti per i differenti sistemi
operativi e per questo consiglio di seguire la documentazione
ufficiale.

Offuscamento
L’offuscamento è un processo molto comune in fase di
release, oltre che estremamente consigliato. L’offuscamento
consiste nel “modificare” il codice binario, rendendo molto più
complessa l’operazione di reverse engineering, poiché funzioni
e classi non abbiano i loro nomi originali, ma delle sigle. Il tool
flutter offre il supporto all’offuscamento in fase di build con il
comando –obfuscation.

Signature
Anche se in modalità differenti, un’app che viene distribuita
per essere verificata dovrà essere necessariamente firmata. Il
processo di firma di un’app consiste innanzitutto nel firmare
digitalmente l’applicazione attraverso l’uso di un keystore locale;
questo servirà per un confronto con le successive release.
I processi di firma e distribuzione, poi, cambiano e sono gestiti
diversamente a seconda dello store da utilizzare. È comunque
un passaggio obbligato.

Continuous delivery
Per quanto il processo di delivery possa essere gestito
manualmente, la scelta migliore rimane quella di automatizzarlo,
rendendo più efficienti sia il processo di sviluppo e i test che la
distribuzione. Se non fosse chiaro il concetto, immaginate quanti
“comandi e/o passaggi” dovrete eseguire per consentire il
rilascio in produzione del vostro applicativo, partendo dal codice
sorgente. Per quanto il Continuous delivery si rifaccia più che
altro a una metodologia, piuttosto che a un tecnicismo, è altresì
vero che per rilasciare “frequentemente” un’applicativo tutto il
processo deve essere automatizzato. È un argomento
estremamente ampio e sto già commettendo l’errore di trattarlo
superficialmente, pertanto vi invito ad approfondire il tema in
autonomia.
Vi lascio solo un riferimento legato all’utilizzo di una tool:
fastlane https://fastlane.tools/. Tra le tante soluzioni che
consentono di approcciare il tema continuous delivery, fastlane,
oltre a essere open-source, offre feature che coprono a 360° il
processo di deployment e non è così complesso mettere in piedi
una pipeline. Vedere di che cosa si occupa il tool vi darà già
un’idea più chiara di quale sia il tema.

Firebase
Uno strumento estremamente potente e forse imprescindibile
è Firebase. Non voglio introdurvi l’argomento poiché è talmente
vasto che in un capitolo conclusivo non saprei proprio come
affrontarlo. Il consiglio che vi voglio dare è di studiare quali sono
i servizi offerti e valutare l’integrazione con Flutter. Servizi di
push notification, building, deploy, ambiente di test e analitiche
rendono questo servizio di casa Google fondamentale.
Firebase e Flutter sono entrambi figli di Google. Per Firebase
il/i plugin è/sono direttamente sviluppati da Google. Vi lascio il
seguente indirizzo per comprendere quanto sia semplice
integrare i servizi offerti da Firebase nella vostra applicazione
Flutter: https://firebase.google.com/docs/flutter/.

Biometria
Un altro tema caldo che non abbiamo affrontato è l’uso della
biometria nelle fasi di autenticazione. La biometria è una
disciplina e può essere immaginata come una misurazione
biofisica con lo scopo di identificare univocamente un individuo.
Negli ultimi anni la biometria ha avuto un ruolo fondamentale nel
rivoluzionare i processi di autenticazione degli utenti in
applicazioni di ogni tipo: web, mobile, desktop, embedded.
Nel campo di un’autenticazione biometrica, l’accesso ad
un’app può essere eseguito tramite l’uso del fingerprint
(impronta digitale) o del riconoscimento facciale in alternativa a
quanto siamo abituati, ovvero con username e password. Il
riconoscimento biometrico in campo tecnologico è sempre in
fase di sviluppo e ricerca. Per esempio, non è inusuale trovare
applicazioni che supportino la biometria vocale, in cui l’utente
viene identificato tramite timbro vocale.
L’uso di autenticatori biometrici è un argomento complesso
prettamente tecnico che, però, nasconde anche temi di
sicurezza di grande importanza. Comunque sia, noi utenti mobili
siamo già abituati all’uso di questi scenari e Flutter, tramite il
plugin local_auth (https://pub.dev/packages/local_auth), offre la
possibilità di usufruire di “autenticatori” alternativi alle password.
A oggi, gli autenticatori biometrici supportati sono:
BiometricType.face BiometricType.fingerprint,
BiometricType.weak, BiometricType.strong. Weak e strong
identificano di fatto una tipologia di autenticazione biometrica
debole e forte che, per esempio, in Android corrispondono
rispettivamente al face recognition e al fingerprint.
Il plugin local_auth offre anche autenticatori non biometrici
come il passcode o PIN.
Risorse utili

Innanzitutto vi invito a documentarvi sulla sezione showcase


di Flutter, dove vengono mostrati i casi di successo di aziende
che adottano il framework per lo sviluppo dei loro progetti:
https://flutter.dev/showcase. Davvero molto interessante sbirciare

chi e come stia usando Flutter, soprattutto per le big companies!


Ecco alcuni indirizzi che, rigorosamente, devono popolare i
vostri preferiti:
https://dart.dev/guides

https://flutter.dev/

https://docs.flutter.dev/

https://pub.dev/

https://github.com/flutter/flutter

https://www.youtube.com/@flutterdev

https://firebase.google.com/docs/flutter/

https://api.flutter.dev/

https://github.com/vgflutter/flutter_apogeo

https://flutter.gskinner.com/folio/

Supporto:
https://flutter.dev/community
Vi invito inoltre a seguire il sito https://www.apogeonline.com/
poiché verranno pubblicati articoli di approfondimenti in tema
Flutter anche sul sito di Apogeo.
Indice

Introduzione
Nativo e ibrido
Punti di forza
A chi è rivolto Flutter
I revisori
Il codice
Ringraziamenti
Capitolo 1 - Installazione
Flutter doctor
Android
iOS
Emulazione
Capitolo 2 - Hello world
Desktop
Browser
Device
Hello World
Build
Capitolo 3 - Dart in teoria
Aspetti peculiari
Esecuzione
Mobile
Flutter
Capitolo 4 - Linguaggio
Aspetti peculiari
Variabili
Tipi di dati
Funzioni
Classi
Istruzioni di controllo
Eccezioni
Moduli
Iterable
Operazioni asincrone
Capitolo 5 - Widget
MyApp widget
MyHomePage widget
Widget
Lifecycle
Widget Base
Capitolo 6 - Routing
Navigator
Named Routes
Passaggio dati
Navigator 2.0
Deep linking
Capitolo 7 - Widget Material Design
Material
Capitolo 8 - Scrollable Widget
Scroll
ReorderableListView
Capitolo 9 - Layout Widget
Vincoli
Single-child
Multi-child
Capitolo 10 - Animazioni
Controller
Ticker
Tipologie
Capitolo 11 - Gesture
GestureDetector
Advanced Gesture
InkWell
Capitolo 12 - Demo
API Platform
Custom Icons
Http
Struttura
Release
Profile
Esercizi per casa
Capitolo 13 - Test
Unit Test
Widget Test
Integration Test
Coverage
Capitolo 14 - Rendering
Widget tree
Element tree
RenderObject tree
Keys
RepaintBoundary
CustomPainter
Capitolo 15 - Layout Design
Responsive
Adaptive
Capitolo 16 - State Management
Design pattern
Provider
BLoC
Redux
Capitolo 17 - Approfondimenti
Cupertino widgets
Data storage
Deploy
Continuous delivery
Firebase
Biometria
Risorse utili

Potrebbero piacerti anche