Il problema del boilerplate ripetuto
In qualsiasi progetto Flutter maturo la creazione di una nuova feature segue uno schema prevedibile: una cartella data con repository e data source, una domain con i modelli, una presentation con la pagina, il controller dello stato e i widget. A questo si aggiungono i file di barrel export, i test scheletro, magari la registrazione della rotta in go_router.
Il risultato è che ogni sviluppatore passa 20 minuti a copiare e incollare da una feature esistente, rinominando classi a mano. È un lavoro noioso, soggetto a errori e — soprattutto — è il modo più rapido per far divergere le convenzioni interne al team.
Mason risolve esattamente questo problema. È un tool CLI (nato da Felix Angelov, lo stesso autore di bloc) che permette di definire template di codice chiamati brick, parametrizzarli con variabili e generarli con un comando. Non è un code generator a runtime come build_runner: Mason lavora una volta sola, al momento della creazione dei file, e il codice prodotto è normalissimo codice Dart che poi vive di vita propria.
Installazione e primi passi
Mason si installa come eseguibile globale di Dart:
dart pub global activate mason_cli
mason --version
Assicurati che ~/.pub-cache/bin sia nel PATH. Dentro al progetto Flutter si inizializza l'ambiente:
mason init
Il comando crea un file mason.yaml nella root, che funziona come il pubspec.yaml dei brick: elenca i template disponibili per quel progetto.
bricks:
# brick pubblicato su brickhub.dev
cubit:
git:
url: https://github.com/felangel/mason.git
path: bricks/cubit
# brick locale, versionato nel repository
feature:
path: bricks/feature
Dopo ogni modifica si scaricano le dipendenze e si verifica la lista:
mason get
mason list
Per generare codice da un brick si usa mason make:
mason make feature --feature_name checkout -o lib/features
Se non passi le variabili da riga di comando, Mason te le chiede in modo interattivo. Con -o indichi la directory di output e con --on-conflict decidi cosa fare se un file esiste già (prompt, overwrite, append, skip).
Creare il proprio brick
Il vero valore arriva quando i brick rispecchiano l'architettura del tuo team. Si parte da:
mason new feature --desc "Genera una feature completa a tre livelli"
Viene creata la struttura:
bricks/feature/
├── brick.yaml
├── README.md
└── __brick__/
└── HELLO.md
La cartella __brick__ contiene i file template: tutto ciò che sta lì dentro viene copiato nell'output, con nomi e contenuti processati tramite mustache.
Dichiarare le variabili
Il file brick.yaml descrive il brick e soprattutto le sue variabili:
name: feature
description: Genera una feature a tre livelli (data/domain/presentation)
version: 0.1.0+1
environment:
mason: ">=0.1.0-dev <0.1.0"
vars:
feature_name:
type: string
description: Nome della feature in snake_case
prompt: Come si chiama la feature?
default: home
state_management:
type: enum
values:
- riverpod
- bloc
- none
default: riverpod
prompt: Quale soluzione di state management?
with_repository:
type: boolean
default: true
prompt: Vuoi generare anche il repository?
endpoints:
type: array
values:
- list
- detail
- create
prompt: Quali endpoint ti servono?
I tipi supportati sono string, number, boolean, enum, array e list. Questo significa che il brick può diventare una vera piccola procedura guidata.
Template e lambda
Nei nomi dei file e nel loro contenuto puoi usare le variabili e le lambda di case conversion, che sono la caratteristica più comoda di Mason. Le più usate sono snakeCase, camelCase, pascalCase, paramCase, constantCase, titleCase, pathCase, sentenceCase.
Un file chiamato __brick__/{{feature_name.snakeCase()}}/presentation/{{feature_name.snakeCase()}}_page.dart genera, per feature_name = checkout, il file checkout/presentation/checkout_page.dart. Il contenuto:
import 'package:flutter/material.dart';
{{#with_repository}}import 'package:flutter_riverpod/flutter_riverpod.dart';{{/with_repository}}
/// Pagina principale della feature {{feature_name.titleCase()}}.
class {{feature_name.pascalCase()}}Page extends StatelessWidget {
const {{feature_name.pascalCase()}}Page({super.key});
static const routeName = '/{{feature_name.paramCase()}}';
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('{{feature_name.titleCase()}}')),
body: const Center(child: Text('TODO: {{feature_name.titleCase()}}')),
);
}
}
Le sezioni mustache {{#variabile}} ... {{/variabile}} funzionano come condizionali per i booleani e come cicli per gli array; {{^variabile}} ... {{/variabile}} è il ramo negativo. Esempio su un repository:
abstract interface class {{feature_name.pascalCase()}}Repository {
{{#endpoints}}
// operazione: {{.}}
{{/endpoints}}
{{#with_repository}}
Future<List<{{feature_name.pascalCase()}}>> fetchAll();
{{/with_repository}}
}
Un dettaglio importante: se un file del template genera un nome vuoto (per esempio perché la variabile condizionale è falsa), quel file non viene creato. Si può quindi condizionare l'intera esistenza di un file inserendo nel nome una sezione mustache, ad esempio {{#with_repository}}{{feature_name.snakeCase()}}_repository.dart{{/with_repository}}.
I partial
Per evitare duplicazione tra template, Mason supporta i partial: file il cui nome inizia con ~ (ad esempio __brick__/~header.dart) che non vengono generati ma possono essere inclusi altrove con {{> header.dart }}. Perfetti per header di licenza, import comuni o blocchi // GENERATED ripetuti.
Gli hook: logica Dart prima e dopo la generazione
I brick possono eseguire codice Dart arbitrario grazie agli hook. Si crea la cartella hooks con un suo pubspec.yaml che dipende dal package mason:
name: feature_hooks
environment:
sdk: ">=3.0.0 <4.0.0"
dependencies:
mason: ^0.1.0-dev
Il file hooks/pre_gen.dart viene eseguito prima della generazione e può aggiungere o modificare variabili:
import 'package:mason/mason.dart';
void run(HookContext context) {
final name = context.vars['feature_name'] as String;
context.vars = {
...context.vars,
'generated_at': DateTime.now().toIso8601String(),
'is_riverpod': context.vars['state_management'] == 'riverpod',
};
context.logger.info('Genero la feature "$name"...');
}
Nota is_riverpod: poiché mustache non sa confrontare stringhe, il trucco standard è precalcolare dei booleani nel pre-gen e usarli come sezioni nei template.
Il file hooks/post_gen.dart è il posto giusto per formattare, installare dipendenze o lanciare build_runner:
import 'dart:io';
import 'package:mason/mason.dart';
Future<void> run(HookContext context) async {
final progress = context.logger.progress('Formattazione e dipendenze');
await Process.run('dart', ['format', '.'], runInShell: true);
if (context.vars['is_riverpod'] == true) {
await Process.run(
'dart',
['run', 'build_runner', 'build', '--delete-conflicting-outputs'],
runInShell: true,
);
}
progress.complete('Feature pronta!');
context.logger.success(
'Ricordati di registrare la rotta in app_router.dart',
);
}
Gli hook hanno accesso a context.logger (con info, warn, err, success, progress, prompt, confirm) e possono quindi diventare piccoli wizard. Attenzione però a non abusarne: un hook che modifica file esistenti del progetto è potente ma fragile, perché dipende dalla forma esatta di quei file.
Distribuire i brick al team
Ci sono tre strategie, in ordine di accoppiamento crescente con il repository:
- Brick locali (
path: bricks/feature): vivono nel monorepo, si versionano insieme al codice e si evolvono con l'architettura. È la scelta migliore per i brick specifici di un singolo prodotto. - Brick su Git: un repository
company_brickscondiviso tra più progetti, referenziato congit: { url: ..., path: ..., ref: v1.2.0 }. Usa sempre unreffissato a un tag per evitare che una modifica upstream cambi il codice generato dalla sera alla mattina. - Brick su BrickHub: il registry pubblico (
brickhub.dev). Si pubblica conmason loginemason publish, e si consuma con una semplice versione (feature: ^0.1.0). Ottimo per brick open source o per un design system condiviso.
Esiste anche mason bundle, che impacchetta un brick in un singolo file (Dart o universale) da includere dentro una CLI personalizzata: è il meccanismo con cui tool come very_good_cli incorporano i propri template.
mason bundle bricks/feature -t dart -o lib/src/bundles/
Integrazione nel flusso di lavoro
Un brick è codice a tutti gli effetti e come tale va mantenuto. Alcune pratiche che funzionano bene:
- Testa la generazione in CI. Un job che esegue
mason make feature --feature_name sample -o build/tmpseguito dadart analyzesul risultato intercetta subito template che generano codice non compilabile dopo un aggiornamento di Flutter o di un package. - Usa un file di configurazione per gli scenari ricorrenti:
mason make feature -c feature_config.jsonlegge le variabili da JSON, utile negli script e nei test. - Mantieni i brick piccoli e componibili. Meglio tre brick (
feature,repository,widget) che un unico mega-template con quindici variabili: la combinatoria esplode e diventa impossibile da mantenere. - Documenta ogni brick nel suo README, includendo un esempio dell'output generato. Mason inserisce automaticamente l'output di esempio se usi
mason makecon--output-dirnel README. - Non generare ciò che può essere una classe base. Se vedi che lo stesso codice identico viene generato in ogni feature senza variazioni, probabilmente appartiene a un package condiviso, non a un template. Mason serve a generare la struttura variabile, non a duplicare logica.
Quando Mason non serve
Per completezza: Mason non sostituisce build_runner. Se hai bisogno di codice che resti sincronizzato con una sorgente di verità (modelli JSON, freezed, riverpod_generator, pigeon), ti serve la generazione a build time. Mason copre il caso opposto, cioè lo scaffolding iniziale di codice che poi verrà modificato a mano.
Allo stesso modo, in un progetto piccolo con due o tre schermate l'investimento nella scrittura dei brick non si ripaga. Il punto di pareggio arriva quando il team ha più di due o tre sviluppatori, oppure quando il numero di feature cresce a doppia cifra: lì la coerenza strutturale vale molto più del tempo risparmiato.
Conclusioni
Mason trasforma le convenzioni implicite del team in template eseguibili. Il vantaggio non è solo la velocità: è che l'architettura smette di essere una pagina di wiki che nessuno legge e diventa qualcosa che il tool applica automaticamente a ogni nuova feature. Inizia con un brick semplice — anche solo uno per i widget riutilizzabili — mettilo nel repository, e lascia che cresca insieme al progetto.