In queste lezioni abbiamo preso familiarità con il framework installandolo sulla nostra macchina, impostando l’ambiente di sviluppo e creando la nostra prima app. In particolare, abbiamo visto la struttura del codice che viene generato automaticamente alla creazione di un nuovo progetto e utilizzato due delle principali funzionalità offerte, ossia Hot Reload e Flutter Inspector. Come abbiamo potuto notare, Dart è il linguaggio principe di questo framework e per poter affrontare al meglio questa guida in questa lezione muoveremo i primi passi con Dart, imparando la sintassi e i costrutti di base di questo nuovo linguaggio di programmazione.
Cos’è
Dart è un linguaggio di programmazione orientato agli oggetti per il web, completamente open source e sviluppato da Google. Presentato per la prima volta nel 2011, Dart è stato rilasciato in versione stabile (v1.0.0) solo qualche anno dopo, e a Novembre del 2018 è stata annunciata la versione 2.1.
Lo scopo di Dart è quello di aiutare lo sviluppatore a costruire moderne applicazioni web, server e mobile cross-platform con Flutter. Per fare ciò, Dart offre tutti gli attributi necessari per un linguaggio moderno e generico, una Virtual Machine per la compilazione del codice Dart, librerie di base e repository di gestione dei pacchetti per l’aggiunta di funzionalità.
Questo linguaggio offre inoltre i seguenti benefici:
Beneficio | Descrizione |
---|---|
Produttività | Sintassi chiara e concisa e cicli di sviluppo quasi istantanei |
Velocità | Tempi di avvio ed esecuzione eccellenti e prevedibili anche su dispositivi con poche risorse hardware |
Portabilità | Dart è un linguaggio flessibile e può essere eseguito su molteplici sistemi operativi desktop (Windows, macOS, Linux) e mobile (Android e iOS) senza alcuna limitazione |
Reactive Programming | La natura reattiva di Dart è dovuta al suo generational garbage collector, alla rapida assegnazione degli oggetti, e al supporto della programmazione asincrona. Ciò rende Dart un buon candidato per creare un'applicazione interattiva o ad alta intensità di dati |
Compilazione | Secondo la modalità Ahead Of Time (AOT), ma supporta anche la compilazione Just In Time (JIT) per cicli di sviluppo veloci |
Proprio per questi benefici, Dart viene utilizzato sempre di più da diverse aziende e progetti di grande calibro.
Installazione
In base all’applicazione o al servizio che vogliamo sviluppare possiamo installare Dart in modo diverso.
Per lo sviluppo di applicazioni mobile con Flutter, Dart viene installato durante l’installazione del framework stesso, come visto ad esempio in una lezione precedente di questa guida.
Contrariamente, nel caso di un’applicazione web o server basterà semplicemente installare il pacchetto di Dart, come descritto nella documentazione ufficiale. Inoltre, Dart può essere installato ed eseguito su Windows, Linux e macOS.
Ambienti di sviluppo
È possibile sviluppare la propria applicazione o servizio in Dart utilizzando uno qualsiasi dei seguenti ambienti di sviluppo:
Ciò è possibile grazie all’installazione del plugin di Dart, come visto in precedenza nel corso di questa guida.
Qualora invece volessimo esplorare le potenzialità del linguaggio, come in questa lezione, invece di creare un nuovo progetto o di aggiungere del codice al progetto creato nelle precedenti lezioni, utilizzeremo DartPad.
DartPad è un servizio online per Dart, pensato per essere un modo semplice e facile per gli utenti per esplorare questo linguaggio e valutarne le principali librerie e funzionalità on-line senza alcuna installazione. DartPad, inoltre, supporta le principali librerie di Dart fatta eccezione delle librerie VM come dart:io
.
Ecco come appare un nuovo Pad aperto su DartPad.
La schermata è semplice e intuitiva. A sinistra troviamo l’area di testo in cui scrivere il nostro codice, mentre a destra troviamo la console in cui viene mostrato l’output. Nella barra superiore, invece, abbiamo accesso a diverse opzioni come:
- creazione di un nuovo Pad;
- formattazione;
- condivisione del Pad attraverso Git;
- accesso ad un insieme di esempi da guardare sotto la voce Samples.
Introduzione al linguaggio
Entriamo adesso nel vivo del linguaggio e vediamone i diversi aspetti.
Stile del codice e Keyword
Uno degli aspetti fondamentali per poter scrivere un codice chiaro, pulito e leggibile a tutti è apprendere lo stile di quello specifico linguaggio di programmazione. Infatti, l’utilizzo di uno stile coerente a quello scelto per quel linguaggio permette una più semplice collaborazione tra gli sviluppatori.
Per quanto concerne l’ecosistema di Dart, si predilige principalmente lo stile CamelCase (lower e Upper) e il lowercase_with_underscores come segue:
Stile | Descrizione | Esempi |
---|---|---|
UpperCamelCase | Capitalizzazione della prima lettera di ogni parola a partire dalla prima. Questo stile è utilizzato per la definizione di classi, enums, e typedefs |
|
lowerCamelCase | Capitalizzazione della prima lettera di ogni parola ad eccezione della prima che è sempre in minuscolo. Questo stile è utilizzato per la definizione di istanze della classe, funzioni, definizioni di livello superiore, variabili, e parametri. |
|
lowercase_with_underscores (snake_case) | Tutte le parole (compresi gli acronimi) sono composte solo da lettere minuscole, e le parole sono separate con _ . Questo stile è utilizzato per i nomi delle librerie. |
|
A questo si aggiunge un set di parole chiave che è importante conoscere per poter lavorare correttamente con il linguaggio. Alcune di queste keyword le abbiamo già incontrate durante l’analisi del codice di un’app Flutter, ma tra quelle più comuni ci sono:
Keyword | Descrizione |
---|---|
abstract , extends |
per la definizione delle classi astratte e l’ereditarietà |
interface , implements |
per la definizione e l’implementazione di interfacce |
if , else |
per la definizione di strutture condizionali |
for , while |
per la definizione dei cicli |
La lista completa di keyword è disponibile sulla documentazione ufficiale.
Tipi
Uno degli aspetti più interessanti di Dart è il fatto che sia dinamicamente tipizzato e che sia possibile scrivere il nostro codice usando o meno le annotazioni sul tipo. In alcuni scenari è conveniente utilizzare i tipi per:
- evitare possibili errori di identificazione di possibili errori;
- migliorare la documentazione rendendola più semplice da leggere;
- definire variabili pubbliche o private e nei parametri delle funzioni.
Di contro, è sconsigliato utilizzare i tipi all’interno delle funzioni.
Di seguito alcuni tipi supportati da Dart.
Tipo | Descrizione | Keyword |
---|---|---|
Integer | Rappresentazione di numeri interi senza valori decimali | int |
Double | Rappresentazione a 64-bit di numeri decimali | double |
String | Rappresentazione di una sequenza di caratteri | String |
Boolean | Rappresentazione di valori booleani, true e false |
bool |
Dynamic | Se il tipo di una variabile non è specificato esplicitamente, il tipo della variabile è dinamico | dynamic |
Commenti
I commenti legati al codice sono uno degli aspetti fondamentali nello sviluppo di una qualsiasi applicazione. In Dart esistono due tipologie di commento.
Tipologia | Descrizione | Esempio |
---|---|---|
Commento a linea singola | Qualsiasi testo tra il // e la fine della riga è considerato un commento |
// single-line comment |
Commento multilinea | Qualsiasi testo, anche su più righe, compreso tra /**/ è considerato un commento multilinea |
/* multiline |
Variabili
Come in tutti i linguaggi di programmazione, è possibile definire delle variabili, dette identificatori, contenenti dei valori. In particolare, in Dart ci sono alcune semplici regole da seguire nella definizione di un identificatore. Un identificatore:
- non può essere una keyword;
- può contenere valori alfanumerici;
- non può contenere spazi;
- non può iniziare con un numero.
Vediamo la definizione dei principali tipi di variabili definiti in precedenza.
String name = 'Antonio';
int age = 30;
double points = 1232.73215;
bool isAuthor = true;
In questo modo abbiamo definito in modo semplice le variabili name
, age
, points
, e isAuthor
tipizzandole in base alle nostre esigenze, ma Dart supporta il type-checking e offre la possibilità di definire delle variabili attraverso la keyword var invece di usare un tipo predefinito come segue.
var emptyVal = '';
Questa assegnazione fa si che il tipo di emptyVal
sia String
. Attraverso il type-checking, infatti, Dart si assicura che quella variabile conterrà solo ed esclusivamente quel tipo di dati. Di fatto, se provassimo ad assegnare il valore 1
alla variabile emptyVal
, verrebbe sollevato un errore di compilazione.
Infine, di default una variabile non inizializzata è impostata a null
. Ad esempio, se eseguiamo nella funzione main()
le seguenti istruzioni
int age;
print(age);
otterremo il valore null
.
Strutture Dati
Come tutti i linguaggi di programmazione Dart offre diverse strutture dati come liste e mappe. Vediamole brevemente.
La creazione e gestione di una lista è alquanto semplice con Dart, in quanto viene definita come un gruppo ordinato di oggetti. È possibile definire una nuova lista di elementi come segue
List<String> list = new List();
In questo modo viene definita una lista di oggetti String
non limitata in lunghezza. In alternativa possiamo definirla anche così:
var list = [];
Se volessimo trattare la lista più come un array a dimensione fissa basterà aggiungere
var articles = [];
articles.add("Flutter");
articles.add("Dart");
print(articles);
articles.remove("Dart");
print(articles);
Per quanto riguarda le mappe, queste sono l’analogo dei dizionari in Python o delle mappe in Java e sono definite come una lista di coppie chiave / valore dove sia le chiavi che i valori possono essere di qualsiasi tipo. Una mappa è una raccolta dinamica e può essere modificata a runtime. L'inizializzazione di una mappa può essere fatta come segue:
var info = new Map();
var info = {};
questo comporta la generazione di un oggetto Map<dynamic, dynamic>
dove sia la chiave che il valore sono di tipo dynamic
.
Se invece vogliamo popolare la nostra mappa con dei dati in nostro possesso, possiamo definire una mappa con le coppie chiave-valore come segue.
var identifier = { key1:value1, key2:value2 [,…..,key_n:value_n] }
Di seguito, riportiamo un semplice utilizzo delle mappe.
var info = new Map();
info['name'] = 'Antonio';
info['surname'] = 'Tedeschi';
info['age'] = 30;
In questo caso abbiamo creato una nuova mappa vuota che abbiamo popolato manualmente con tre coppie chiave-valore dove il tipo dei valori è eterogeneo tra loro. Con questa inizializzazione, la mappa ha sia la chiave che il valore di tipo dynamic
. Contrariamente, se inizializzassimo la mappa con dei valori di tipo String
e successivamente volessimo aggiungere una nuova chiave con un valore di tipo int
, il compilatore solleverebbe un’eccezione relativa al diverso tipo in quanto la mappa sarebbe definita come Map<String, String>
, come riportato nell’esempio.
var info = {‘name’: 'Antonio'};
info[‘id’] = 1;
//error - A value of type 'int' can't be assigned to a variable of type 'String'.
Se invece la mappa viene inizializzata già con i valori di diverso tipo, avremo che la mappa sarà Map<String, Object>
e potremo aggiungere altri valori con tipi eterogenei.
Costrutti condizionali
Per quanto riguarda i costrutti condizionali, la sintassi è simile a quella di Java. Ad esempio per quanto riguarda il costrutto if-else
abbiamo
if (condizione1) {
//corpo1
} else if (condizione2) {
//corpo2
} else {
//corpo3
}
dove la condizioneX
è la condizione che viene analizzata per determinare quale corpo (o meglio insieme di operazioni) andare ad eseguire. Ad esempio, supponendo di voler definire la categoria di appartenenza di una persona in base all’età avremo il seguente costrutto condizionale.
var age = 45;
if (age < 30) {
print('young');
} else if (age < 65) {
print('adult');
} else {
print('elder');
}
Oltre al costrutto if-else
, è possibile utilizzare il costrutto switch
che permette di controllare tanti valori per una singola variabile. Generalizzando, può essere definito come segue.
switch (identifier) {
case value
{
// corpo
}
break;
default:
{
// fallback
}
break;
}
Diversamente da molti altri linguaggi, la condizione di break
è posta al di fuori del blocco di operazioni da eseguire rispetto ai relativi case. Vediamo, quindi, un caso pratico in cui vogliamo verificare il temperamento di una persona in base alla sua categoria di età.
var category = "adult";
switch (category) {
case "young":
{
print("impetuous");
}
break;
default:
{
print("wise");
}
break;
}
Cicli
In Dart, esistono due tipi di cicli (anche detti loop):
for loop |
esegue una iterazione per ogni elemento di un iterabile |
while loop |
itera fintanto che una condizione è verificata |
In particolare, il ciclo for
ci permette di iterare su tutti gli elementi di un insieme ed eseguire un’operazione. Ad esempio, con il seguente ciclo for
è possibile stampare in console i valori della variabile i
.
for (var i = 0; i < 5; i++) {
print(i);
}
Nel caso di iterazione su una lista di elementi è possibile usare l’istruzione for-in
var collection = ['a', 'b', 'c'];
for (var x in collection) {
print(x);
}
oppure possiamo utilizzare il metodo forEach
offerto dalla classe Iterable<E>
come segue.
collection.forEach((el) => print(el));
Il risultato in entrambi i casi sarà analogo.
Diversamente dal ciclo for
, il ciclo while itera fintanto che una condizione è vera. Ad esempio, nel seguente esempio il ciclo while
stamperà i valori della variabile i
finché i
non raggiunge il valore di 5
.
var i = 0;
while (i < 5) {
print(i);
i++;
}
Funzioni
Le funzioni permettono di raggruppare un insieme di istruzioni per eseguire un compito specifico. In particolare, una funzione accetta in input zero o più parametri, li elabora, e restituisce in output un risultato.
In generale, con Dart possiamo definire una funzione come segue
int countArticles(List<String> articles) {
return articles.length;
}
In questo caso, abbiamo definito una funzione che, data una lista di stringhe, restituisce il numero di articoli in essa contenuta. Sempre in questo caso, abbiamo definito per maggiore chiarezza il tipo di ritorno della funzione e il tipo del parametro. Tuttavia, con Dart la stessa funzione può essere scritta anche senza definirne i tipi.
countArticles(articles) {
return articles.length;
}
Se la funzione contiene una sola istruzione, possiamo riscriverla in modo più compatto
countArticles(articles) => articles.length;
Classi
Ultimo aspetto, ma non per importanza, è la definizione di una classe. Come in tutti i linguaggi di alto livello, una classe è identificabile come un tipo di dato astratto che può rappresentare un’entità nella vita reale ed è composta da attributi e metodi che la definiscono.
Definiamo una classe Author
che ci permette di descrivere un autore con tre attributi: nome, cognome e una lista di articoli rappresentati con il loro nome.
La classe sarà quindi definita così.
class Author {
String name;
String surname;
var articles = [];
}
Come in Java, il modo più comune per definire il costruttore è il seguente:
class Author {
//. . .
Author(String name, String surname){
this.name = name;
this.surname = surname;
}
}
In alternativa a questa definizione, Dart offre un modo più compatto per definire il costruttore della classe:
Author(this.name, this.surname);
A questo punto aggiungiamo alcuni metodi di supporto per:
- aggiungere un nuovo articolo alla lista di quelli dell’autore;
- mostrare questa lista;
- ottenere il numero totale di articoli.
class Author {
// . . .
void addArticle(String article) {
articles.add(article);
}
void showArticles() {
articles.forEach((article) => print(article));
}
countArticles() => articles.length;
}
Infine, facciamo l’override del metodo toString()
.
class Author {
// . . .
@override
String toString() => 'Author: $name $surname';
}
Attraverso questa implementazione, ogni qualvolta verrà effettuata la stampa di un oggetto di tipo Author
verrà stampato il valore degli attributi name
e surname
invece della dicitura
Instance of 'Author'
Testiamo adesso quanto abbiamo scritto e creiamo un nuovo oggetto Author
per vederne il funzionamento.
void main() {
var author = Author('Antonio', 'Tedeschi');
print(author.name);
print(author);
author.addArticle('Dart');
author.addArticle('Flutter');
author.showArticles();
print(author.countArticles());
}
In questo esempio, abbiamo:
- creato un nuovo autore;
- stampato solo il nome dell’autore accedendo al suo attributo
name
; - stampato l’oggetto author che di default invoca il metodo
toString()
ridefinito da noi; - aggiunto due articoli e invocato il relativo metodo per mostrare la lista di articoli;
- stampato il numero totale di articoli di quell’autore.
Visibilità
Diversamente da Java, Dart non ha alcuna parola chiave come public
, protected
o private
per definire la visibilità di un identificatore, ossia variabile, metodo o classe. Di default qualsiasi identificatore è pubblico e accessibile. Per rendere un identificatore privato per la sua classe o libreria (in base alla natura dell’identificatore) è necessario anteporre al nome dell’identificatore il carattere underscore _
.
Di seguito un semplice esempio.
class _MyClass {
var _myVar;
void _myFn() {
print('0');
}
}
Per ulteriori informazioni sul linguaggio si rimanda alla documentazione ufficiale.