Nessun risultato. Prova con un altro termine.
Guide
Notizie
Software
Tutorial
  • Lezione 9 di 37
  • livello intermedio
Indice lezioni

Il framework: dettagli tecnici

Imparare a conoscere come funziona il framework Flutter, che permette di realizzare app mobile multipiattaforma con un'unica codebase.
Imparare a conoscere come funziona il framework Flutter, che permette di realizzare app mobile multipiattaforma con un'unica codebase.
Link copiato negli appunti

Nella prima lezione di questa guida, abbiamo brevemente introdotto Flutter e il suo funzionamento, paragonandolo a quello di un’applicazione nativa e analizzando le differenze con Xamarin, noto framework per lo sviluppo di app cross-platform.

In questa lezione, entreremo nel vivo di Flutter analizzando nel dettaglio alcuni aspetti essenziali del framework e la sua architettura stratificata che ne permette il funzionamento.

Comunicazione tra un’app Flutter e una piattaforma mobile

Rispetto ad altri framework, Flutter è costruito in un modo completamente nuovo, permettendo di creare applicazioni semplici e ad alte prestazioni. Per rendere possibile ciò, Flutter viene eseguito su ogni piattaforma in modo nativo usando la compilazione AOT, mentre durante la fase di sviluppo, per rendere il processo di test più veloce, viene utilizzata la compilazione JIT.

Partendo da una rappresentazione ad alto livello, ogni applicazione basata su Flutter è composta da Widget che possono essere contenitori, testi, immagini e molto altro ancora. Ogni widget viene interpretato e rappresentato su una Canvas gestita dal motore grafico di Skia. La piattaforma mostra il widget così costruito all’utente finale ed intercetta e inoltra gli eventi scaturiti dall’interazione all’app.

Figura 49. Rappresentazione ad alto livello della comunicazione tra un’app Flutter e la piattaforma mobile di interesse (click per ingrandire)


Rappresentazione ad alto livello della comunicazione tra un’app Flutter e la piattaforma mobile di interesse

Architettura

Scendiamo ad un livello di granularità maggiore e analizziamo nel dettaglio la struttura dell’architettura definita per Flutter, mostrata nella seguente figura.

Figura 50. Architettura stratificata di Flutter (click per ingrandire)


Architettura stratificata di Flutter

L’architettura di Flutter si compone di tre macro blocchi principali composti a loro volta da API e librerie che caratterizzano ogni strato. A grandi linee, questo tipo di architettura ricorda molto quella proposta da Android, ma vediamola nel dettaglio.

Strato Descrizione
Embedder - Platform Specific

È il livello più basso dell’architettura di Flutter ed è il cuore dell’Engine di Flutter.

In questo strato vengono definiti gli embedder specifici per le piattaforme, che hanno lo scopo di legare tra loro il rendering al toolkit della schermata nativa, alla gestione degli eventi di input, etc. Per fare ciò, gli embedder interagiscono con il layer di Engine tramite delle API C/C++ di basso livello. Tali API, però, sono esposte solo internamente.

Qualora lo sviluppatore debba implementare un particolare comportamento, può utilizzare delle API di integrazione ad alto livello per le piattaforme Android e iOS.

Questo strato si compone, inoltre, di una Shell che ospita la Dart VM. In particolare, la Shell è specifica per ogni piattaforma e offre un accesso alle API native della piattaforma in questione. Infatti, le shell implementano del codice specifico, come la comunicazione con gli Input Method Editor (IME) e gli eventi del ciclo di vita dell'app in base al sistema operativo di interesse.

Engine

Questo strato intermedio è il lato C/C++ di Flutter ed è definito nel repository engine. In particolare, l’Engine include molteplici componenti di basso livello, fondamentali per il funzionamento del framework e delle sue operazioni di base. Tra questi troviamo il motore grafico Skia e le shell a cui è possibile accedere tramite le API esposte dalla libreria dart:ui. In particolare, è possibile creare un’app utilizzando le classi definite in questa libreria come Canvas, Paint e TextBox

Framework

È lo strato più importante per gli sviluppatori e offre tutte le librerie e i pacchetti necessari per lo sviluppo di un’app.

Tra questi vi sono i layer relativi alle animazioni, alla definizione delle gesture, e alla creazione dei widget tramite l’omonimo layer che risulta essere il più importante durante lo sviluppo di un’app. Infatti, il layer Widget permette la definizione dei layer Material e Cupertino per la realizzazione dei componenti grafici secondo lo stile Android e iOS, rispettivamente, e di definire dei Widget personalizzati.

Tendenzialmente, durante lo sviluppo di un’app sarà molto più comune lavorare con i layer di questo strato dell’architettura, andando a comporre widget e animazioni a partire da quelle già fornite da Flutter stesso o creandone delle proprie ad-hoc.

Una descrizione delle varie librerie presenti nello strato Framework è reperibile sulla documentazione ufficiale

L’obiettivo di questa struttura è di aiutare lo sviluppatore a realizzare la propria app riducendo il più possibile la verbosità del codice per farlo.

Analizziamo adesso più nel dettaglio il layer di Widget e di Rendering.

Widget

Come già anticipato nelle prime lezioni introduttive di questa guida, in Flutter tutto può essere definito come un Widget:

  • elementi strutturali come i bottoni o i menu;
  • elementi stilistici come il font;
  • aspetti del layout come il padding;
  • riconoscimento di una gesture tramite widget.

L’aspetto interessante dei widget è che questi formano una struttura gerarchica in cui ognuno di essi annida all'interno e eredita diverse proprietà dal nodo padre.

I widget, quindi, sono spesso composti da altri piccoli widget monouso che vengono combinati per ottenere effetti e risultati più articolati. Ad esempio, il widget Container viene comunemente popolato con un insieme di altri widget responsabili del layout, dello stile, del posizionamento e del dimensionamento degli elementi in esso contenuti.

Inoltre, la struttura gerarchica dei widget è stata pensata in modo da essere poco profonda ma ampia al fine di massimizzare il numero possibile di combinazioni che si possono avere per creare un nuovo widget di tipo Stateful o Stateless.

Figura 51. Esempio di struttura gerarchica dei widget Questa struttura gerarchica semplifica e velocizza le fasi di sviluppo (click per ingrandire)


Esempio di struttura gerarchica dei widget Questa struttura gerarchica semplifica e velocizza le fasi di sviluppo

Rendering

Il layer dei widget è a sua volta costruito a partire dal layer di Rendering, definito in dall’apposita libreria rendering. Tale libreria sfrutta le API offerte dalla libreria dart:ui e implementa un insieme di operazione matematiche complesse per poter disegnare gli elementi che compongono l’interfaccia utente. Ciò è reso possibile tramite la definizione di specifici oggetti come il RenderObject, che risulta essere l’elemento principe del processo di rendering. In particolare, ogni RenderObject ha un nodo padre e ha uno slot chiamato parentData, in cui il nodo padre RenderObject può memorizzare dati specifici come, ad esempio, la posizione del nodo figlio.

Questa peculiarità permette di definire una struttura gerarchica di RenderObject che è alla base del funzionamento della libreria di rendering ed è utilizzata da Flutter per disegnare gli elementi grafici. La definizione di una struttura gerarchica è però un processo complesso e costoso, che Flutter ottimizza usando un algoritmo in grado di tenere in memoria in modo “aggressivo” i calcoli più costosi, al fine di minimizzare il lavoro compiuto ad ogni iterazione.

La classe RenderObject è una classe astratta e necessita di essere estesa da un’ulteriore classe per fare il rendering degli oggetti. In particolare, Flutter definisce un’altra classe di oggetti, i RenderBox.

La classe RenderBox permette di definire un oggetto in un sistema di coordinate cartesiane bidimensionale, ossia all’interno di un box, dove:

  • le dimensioni sono espresse in termini di width e height;
  • le coordinate sono espresse come coppie (width,height);
  • la coordinata (0,0) è definita nell’angolo in basso a destra del box.

Sfruttando le potenzialità di questa classe, è possibile costruire un’interfaccia performante composta da box dove, se l’elemento (es. un bottone o uno switch) all’interno di un box è soggetto a modifiche, il sistema ricalcolerà solo ed esclusivamente quell’elemento.

Processo di rendering dei Widget

Immaginiamo di aver definito una semplice applicazione composta da tre widget:

Widget Descrizione
MyApp è lo StatelessWidget che verrà eseguito dal metodo runApp()
MyContainer è il widget che conterrà il testo e di cui è possibile, ad esempio, definire il colore attraverso la proprietà color. Ad esempio color: Colors.white
MyText è il widget che mostrerà il testo desiderato

Figura 52. Struttura dei widget (click per ingrandire)


Struttura dei widget

Alla prima esecuzione del metodo runApp(), Flutter:

  1. creerà l’albero dei widget (Widget Tree)) contenente i tre widget che abbiamo definito;
  2. definirà un secondo albero (Element Tree) contenente le rispettive istanze dei widget rappresentate tramite la classe Element e ottenute tramite l’invocazione del metodo createElement();
  3. creerà un terzo albero (Render Tree) contenente i RenderObject creati dagli oggetti Element attraverso l’invocazione del metodo createRenderObject().

Ciò porta alla creazione di tre alberi, dove ogni albero è costituito da tre nodi, uno per ogni widget che abbiamo definito in precedenza.

La definizione di questi tre alberi fa si che per ogni Widget Flutter associ un Element e un RenderObject contenuti rispettivamente nell’Element e Render Tree. Entrando nel dettaglio, è proprio l’oggetto Element che, oltre a rappresentare un’istanza di uno specifico Widget e la relativa posizione nell’albero, terrà un riferimento ai relativi Widget e RenderObject.

La definizione di questi tre alberi e delle relative relazioni, rende Flutter molto performante. Infatti, ad ogni cambiamento nella struttura del Widget Tree, Flutter userà l'Element Tree per confrontare il nuovo Widget Tree con i RenderingObject esistenti. Poichè i Widget sono immutabili, ogni modifica nella configurazione dei Widget risulta in una ricostruzione del Widget Tree. Ciò, computazionalmente parlando, non è un’operazione costosa, in quanto i Widget sono leggeri ed economici da istanziare, a differenza dei RenderingObject il cui costo di creazione è elevato.

Ogni volta che vengono effettuate delle modifiche ai Widget, accadranno i seguenti eventi:

  • il framework genererà una ricostruzione del Widget Tree;
  • attraverso gli oggetti Element definiti nell’Element Tree, Flutter verificherà in ordine gerarchico ogni elemento del Widget Tree con la rispettiva rappresentazione nel Render Tree, verificando se il tipo del vecchio e del nuovo Widget sono uguali o meno.
    • Nel caso in cui non sia cambiato, Flutter aggiornerà la configurazione del RenderObject.
    • Nel caso in cui sia cambiato, l’Element e il RenderObject verranno rimossi(e ovviamente i relativi sottoalberi se presenti) e saranno creati i nuovi oggetti.

Immaginiamo, quindi, di aver cambiato il colore di MyContainer da bianco a blu. Flutter:

  • creerà il nuovo albero dei Widget in cui risulta che MyContainer ha cambiato il colore da bianco a blu;
  • verificherà il tipo di ogni widget tramite i relativi Element;
  • si accorgerà che il tipo del widget MyContainer non è cambiato, ma che è stata modificata la sua configurazione e aggiornerà di conseguenza la configurazione del relativo RenderObject;
  • completerà la verifica alla ricerca di eventuali cambiamenti.

Ora che abbiamo un’idea chiara dell’architettura di Flutter e di come funziona il processo di rendering siamo pronti per affrontare al meglio questa guida.

Ti consigliamo anche