2.5 - Approfondimenti su Dart
1. Costruzione di Classi, Ereditarietà e Mixin
Dart è un linguaggio orientato agli oggetti. Qui vediamo come definire una classe, un costruttore e come implementare l’ereditarietà e i mixin.
Esempio: Classe e Costruttore
class Persona {
//Attributi
final String nome; // Questo attributo è dichiarato immutabile
int eta;
// Esempi di costruttori
// Costruttore shorthand
Persona(this.nome, this.eta);
/*
* Questo però richiede che gli attributi siano nullable (Non usato in dart)
Persona(nome,eta){
this.nome = nome;
this.eta = eta;
}
*/
/* Costruttore con valori posizionali e valori nominali
Persona(this.eta,{required this.nome});
*/
//Metodi
void saluta() {
print("Ciao, sono $nome e ho $eta anni.");
}
int getAge() {
return eta;
}
}
void main() {
var p = Persona("Alice", 28);
p.eta = 27;
// p.nome = "Anna"; // Questa riga produce un errore.
p.saluta();
}
Per creare un istanza di una classe come avete notato non è necessario utilizzare la keyword new
questo semplifica la generazione di layout visto che ogni widget che noi usiamo è un istanza di una classe
Visibilità degli attributi in Dart
In Dart, il controllo dell’accesso a variabili e metodi si basa su una convenzione semplice e precisa: l’uso dell’underscore _
.
Pubblico
Per impostazione predefinita, tutti i campi e metodi in Dart sono pubblici. Ciò significa che sono accessibili da qualsiasi punto dell’app, da altri file, classi o package.
Privato con _
In Dart non esiste la parola chiave private
.
Per rendere un membro privato, si antepone un underscore _
al suo nome.
Questo rende il membro privato al file, cioè visibile solo dentro lo stesso file Dart.
class Persona {
String _segreto = '1234';
}
void main() {
final p = Persona();
print(p._segreto); // ❌ Errore se questo codice è in un altro file
}
Dart è progettato per essere semplice, leggibile e facilmente integrabile con altri ambienti, incluso JavaScript (in quanto può essere compilato in JS).
A differenza di Java o C++, Dart non punta su un sistema complesso di visibilità (private
, protected
, internal
, ecc.)
Questo incoraggia l’organizzazione del codice per file: se vuoi nascondere un dettaglio, mettilo nello stesso file e rendilo _privato
.
Getter e Setter in Dart
In Dart, puoi accedere e modificare campi privati in modo controllato usando getter e setter.
Perché usarli?
- Per nascondere l’implementazione interna
- Per controllare/modificare i dati quando vengono letti o scritti
- Per proteggere campi privati (
_variabile
) mantenendo l’accesso pubblico
Sintassi di base
class Conto {
double _saldo = 0;
double get saldo => _saldo;
set saldo(double valore) {
if (valore >= 0) {
_saldo = valore;
}
}
}
Utilizzo
void main() {
final conto = Conto();
conto.saldo = 100.0; // usa il setter
print(conto.saldo); // usa il getter
}
Nota su prestazioni e semplicità
- Getter e setter in Dart non usano parentesi tonde quando li chiami.
- Questo li fa sembrare variabili, ma in realtà sono metodi.
persona.nome = 'Mario'; // chiama il setter
print(persona.nome); // chiama il getter
🧠 Quiz:
Rispondi alle seguenti domande per verificare quanto hai capito.
Cosa significa quando un campo in Dart inizia con _
?
Il seguente getter in Dart è valido? String get nome => _nome;
Cosa succede se accedi a _password
da un file diverso da quello in cui è definito?
Variabili static
In Dart, la parola chiave static
serve per dichiarare variabili o metodi che appartengono alla classe, non alle singole istanze.
Cos’è una variabile static
?
- Una variabile
static
è condivisa da tutte le istanze della classe. - Non serve creare un oggetto per accedervi.
- Si accede con
NomeClasse.nomeVariabile
.
Sintassi base
class Contatore {
static int valore = 0;
}
void main() {
print(Contatore.valore); // ➜ 0
Contatore.valore++;
print(Contatore.valore); // ➜ 1
}
Esempio con istanze
class Persona {
static int numeroTotale = 0;
String nome;
Persona(this.nome) {
numeroTotale++;
}
}
void main() {
var p1 = Persona('Luca');
var p2 = Persona('Anna');
print(Persona.numeroTotale); // ➜ 2
}
La variabile numeroTotale
è incrementata da ogni oggetto, ma è unica per la classe.
Metodi statici
Anche i metodi possono essere statici. Servono per funzioni utilitarie o di calcolo:
class Matematica {
static int quadrato(int x) => x * x;
}
void main() {
print(Matematica.quadrato(4)); // ➜ 16
}
Attenzione
- I metodi
static
non possono accedere a campi non statici della classe.
class Test {
int numero = 5;
static void stampa() {
// print(numero); ❌ Errore!
}
}
Dart non ha classi static come in Java o C#.
Dart ha un approccio semplice e flessibile:
- Se una classe non ha stato interno, non serve istanziarla
- Se non vuoi che venga istanziata, nascondi il costruttore
Il costruttore factory
In Dart, il costruttore factory
ti permette di personalizzare la creazione di un oggetto.
A differenza del costruttore classico, può:
- Restituire un’istanza già esistente
- Applicare logica condizionale o modificare l’input
- Usare costruttori privati per creare oggetti interni alla classe
Sintassi base
class Persona {
final String nome;
// Costruttore privato
Persona._(this.nome);
// Costruttore factory
factory Persona(String nome) {
return Persona._(nome.toUpperCase());
}
}
void main() {
final p = Persona('mario');
print(p.nome); // ➜ 'MARIO'
}
Perché usare factory
?
- Per controllare come e quando viene creato l’oggetto
- Per applicare validazioni o trasformazioni
- Per implementare il pattern Singleton
- Per riutilizzare istanze già esistenti
Confronto: factory
vs costruttore classico
Caratteristica | Costruttore classico | factory constructor |
---|---|---|
Crea sempre una nuova istanza | ✅ | ❌ può restituire oggetto esistente |
Può contenere logica condizionale | ❌ | ✅ |
Può restituire valori diversi | ❌ | ✅ |
Può accedere a costruttori privati | ❌ | ✅ |
Utile per Singleton/cache | ❌ | ✅ |
Esempio: Singleton con factory
class Config {
static final Config _istanza = Config._();
Config._();
factory Config() => _istanza;
}
void main() {
var a = Config();
var b = Config();
print(identical(a, b)); // true
}
Esempio: validazione in factory
class CodiceFiscale {
final String codice;
factory CodiceFiscale(String codice) {
if (codice.length != 16) {
throw FormatException('Codice non valido');
}
return CodiceFiscale._(codice.toUpperCase());
}
CodiceFiscale._(this.codice);
}
Ereditarietà
L’ereditarietà permette a una classe di riutilizzare codice da un’altra classe, estendendola e personalizzandola.
Uso di super
nel costruttore
class Persona {
String nome;
int eta;
Persona(this.nome, this.eta);
}
class Studente extends Persona {
String corso;
Studente(String nome, int eta, this.corso) : super(nome, eta); //chiama il costruttore della superclasse `Persona`
}
Override dei metodi
Puoi sovrascrivere un metodo della superclasse usando @override
.
class Persona {
String nome;
int eta;
Persona(this.nome, this.eta);
void saluta() => print("Ciao, sono $nome e ho $eta anni.");
}
class Studente extends Persona {
String corso;
Studente(String nome, int eta, this.corso) : super(nome, eta);
@override
void saluta() {
print("Ciao, sono $nome, ho $eta anni e studio $corso.");
}
}
Polimorfismo
Puoi usare una variabile di tipo Persona
per contenere un Studente
.
void main() {
Persona p = Studente("Marco", 22, "Informatica");
p.saluta(); // ➜ Chiama comunque il metodo sovrascritto!
}
polimorfismo: il comportamento si adatta al tipo reale dell’oggetto.
Casting: is
, as
, is!
Controllo del tipo con is
if (p is Studente) {
print("È uno studente.");
}
Cast esplicito con as
if (p is Studente) {
Studente s = p as Studente;
print("Corso: ${s.corso}");
}
Negazione con is!
if (p is! Studente) {
print("Non è uno studente.");
}
Esempio completo
class Persona {
String nome;
int eta;
Persona(this.nome, this.eta);
void saluta() => print("Ciao, sono $nome e ho $eta anni.");
}
class Studente extends Persona {
String corso;
Studente(String nome, int eta, this.corso) : super(nome, eta);
@override
void saluta() {
print("Ciao, sono $nome, ho $eta anni e studio $corso.");
}
}
void main() {
Persona p = Studente("Marco", 22, "Informatica");
p.saluta(); // ➜ Chiama metodo di Studente
if (p is Studente) {
Studente s = p as Studente;
print("Corso: ${s.corso}");
}
}
implements
: usare una classe come interfaccia
In Dart, non esiste la parola chiave interface
.
Tuttavia, qualsiasi classe può essere usata come interfaccia grazie alla parola chiave implements
.
Quando una classe implements
un’altra:
- Non eredita l’implementazione
- Deve riscrivere tutti i metodi e le proprietà
- Agisce come se stesse soddisfacendo un contratto
Esempio pratico
class Stampabile {
void stampa() {
print('Stampo...');
}
}
class Fattura implements Stampabile {
@override
void stampa() {
print('Stampo la fattura');
}
}
Differenza tra extends
e implements
Cosa fa? | extends | implements |
---|---|---|
Eredita implementazione | ✅ sì | ❌ no, devi riscrivere tutto |
Richiede override | ❌ no | ✅ sì |
Serve per interfacce | ❌ no (ma possibile) | ✅ sì |
Implementazione multipla
Puoi implementare più classi/interfacce:
class Salva {
void salva() {}
}
class Invia {
void invia() {}
}
class Report implements Salva, Invia {
@override
void salva() => print('Report salvato');
@override
void invia() => print('Report inviato');
}
Le classi abstract
In Dart puoi definire una classe astratta usando la keyword abstract
.
Una classe abstract
serve come base per altre classi, ma non può essere istanziata direttamente.
A cosa serve una classe astratta?
- A definire metodi da implementare obbligatoriamente
- A fornire funzionalità condivise per più sottoclassi
- A definire un modello comune (contratto parziale)
Sintassi base
abstract class Animale {
void parla(); // metodo astratto
}
void main() {
// var a = Animale(); ❌ Errore: una classe astratta non può essere istanziata
}
Può avere anche metodi normali
Una abstract class
può anche contenere metodi con corpo:
abstract class Forma {
void disegna(); // metodo astratto
void descrizione() {
print("Sono una forma geometrica.");
}
}
Confronto: abstract
vs implements
Caratteristica | abstract class | implements |
---|---|---|
Può avere codice | ✅ sì | ❌ no |
Può essere estesa (extends ) | ✅ sì | ❌ no |
Obbligo di override totale | ❌ solo se il metodo è astratto | ✅ sempre |
Esempio d’uso tipico | Base comune con logica condivisa | Interfaccia pura |
Mixins
Un mixin è un modo per riutilizzare codice tra più classi, senza dover usare l’ereditarietà classica (extends
) o le interfacce (implements
).
A cosa servono i mixin?
- Per aggiungere comportamenti comuni a più classi
- Per evitare la duplicazione di codice
- Per non usare ereditarietà multipla, che Dart non supporta
Sintassi base
Per creare un mixin:
mixin Volante {
void vola() => print("Sto volando!");
}
Per usarlo in una classe:
class Uccello with Volante {}
Esempio pratico
mixin Nuotante {
void nuota() => print("Sto nuotando!");
}
mixin Volante {
void vola() => print("Sto volando!");
}
class Anatra with Nuotante, Volante {}
void main() {
var a = Anatra();
a.nuota(); // ➜ Sto nuotando!
a.vola(); // ➜ Sto volando!
}
Limitazioni: on
Se vuoi limitare l’uso di un mixin solo a classi che estendono un certo tipo, puoi usare la keyword on
.
class Animale {}
mixin Coda on Animale {
void scodinzola() => print("Scodinzolo!");
}
class Cane extends Animale with Coda {} // Ok
class Robot with Coda {} // ❌ Errore: Robot non è un Animale
Mixin con stato
Un mixin può avere variabili, ma deve essere usato con attenzione:
mixin Logger {
void log(String msg) {
final now = DateTime.now();
print("[$now] $msg");
}
}
class Servizio with Logger {
void esegui() {
log("Servizio eseguito");
}
}
Questo perché i mixin vengono iniettati nella classe, e ogni classe ne ottiene una copia separata, il che può generare:
- Conflitti di nome tra mixin
- Stato duplicato non intenzionale
- Confusione nella gestione del comportamento
Conflitti di nome tra mixin
mixin A {
String status = 'da A';
}
mixin B {
String status = 'da B'; // ❌ Dart non sa quale usare
}
class C with A, B {} // Errore di ambiguità
Uso corretto dei mixin
Lo stato dovrebbe stare nella classe che usa il mixin.
mixin Logger {
void log(String msg) {
print("LOG: $msg");
}
}
class Servizio with Logger {
int operazioni = 0;
void esegui() {
operazioni++;
log("Operazione n° $operazioni");
}
}
📌 In questo modo, Logger
è riutilizzabile e non introduce stato.
sealed
in Dart
La keyword sealed
permette di creare classi con gerarchie chiuse.
Significa che solo le classi nello stesso file possono estendere o implementare una classe sealed
.
A cosa serve?
- Definire un insieme limitato e controllato di sottotipi
- Forzare la gestione di tutti i casi possibili in
switch
oif
- Impedire l’estensione da parte di altri file o moduli
Sintassi di base
sealed class StatoConnessione {}
class Connesso extends StatoConnessione {}
class Disconnesso extends StatoConnessione {}
class InCaricamento extends StatoConnessione {}
Solo queste classi possono estendere StatoConnessione
perché si trovano nello stesso file.
Uso pratico con switch
void gestisci(StatoConnessione stato) {
switch (stato) {
case Connesso():
print("Utente connesso");
case Disconnesso():
print("Utente disconnesso");
case InCaricamento():
print("Caricamento...");
}
}
Il compilatore sa tutti i sottotipi possibili e ti avvisa se ne dimentichi uno.
Differenza tra abstract
e sealed
Modificatore | Istanziabile? | Estendibile da altri file? | Finalità |
---|---|---|---|
abstract | ❌ No | ✅ Sì | Modello base, libero |
sealed | ❌ No | ❌ Solo nello stesso file | Insieme chiuso di sottotipi |
Quando usarlo?
- Quando vuoi creare uno stato o evento chiuso
- Quando vuoi forzare la gestione completa in un
switch
- Quando vuoi evitare estensioni non desiderate
Nota sulle classi avanzate in Dart
Dart offre diverse keyword per gestire in modo preciso l’estensione, l’implementazione e l’uso delle classi:
abstract
, sealed
, base
, final
, e interface
.
Queste parole chiave permettono di:
- Creare classi che non possono essere estese (
final
) - Definire contratti da implementare (
interface
) - Limitare l’estensione a un solo file (
sealed
) - Controllare gerarchie (
base
) - Definire modelli astratti (
abstract
)
Tuttavia, sarà utile approfondire in futuro ma non sono necessarie queste informazioni adesso.