Il problema delle stringhe magiche

In quasi ogni progetto Flutter prima o poi compare una riga come questa:

Image.asset('assets/images/profile_placeholder.png')

Funziona, finché qualcuno non rinomina il file, non sposta la cartella o non sbaglia una lettera. Il compilatore non dice nulla: l'errore arriva a runtime, spesso in produzione, sotto forma di un rettangolo grigio con l'eccezione Unable to load asset.

Lo stesso vale per i font (fontFamily: 'Montserrat'), per i file JSON di configurazione, per le animazioni Lottie e per le icone SVG. Sono tutte dipendenze reali del codice, ma espresse in un formato che nessuno strumento può verificare.

flutter_gen risolve il problema alla radice: analizza il pubspec.yaml e la cartella degli asset e genera classi Dart tipizzate. Da quel momento Assets.images.profilePlaceholder è un simbolo vero, con autocompletamento, go to definition e refactoring automatico.

Installazione e configurazione

flutter_gen non è una dipendenza runtime: è un generatore. Si aggiunge quindi tra le dev_dependencies.

dev_dependencies:
  build_runner: ^2.4.13
  flutter_gen_runner: ^5.8.0

La configurazione vive direttamente nel pubspec.yaml, sotto la chiave di primo livello flutter_gen:

flutter:
  uses-material-design: true
  assets:
    - assets/images/
    - assets/icons/
    - assets/lottie/
    - assets/json/
  fonts:
    - family: Montserrat
      fonts:
        - asset: assets/fonts/Montserrat-Regular.ttf
        - asset: assets/fonts/Montserrat-Bold.ttf
          weight: 700

flutter_gen:
  output: lib/gen/           # dove finiscono i file generati
  line_length: 80

  integrations:
    flutter_svg: true        # genera il metodo .svg()
    lottie: true             # genera il metodo .lottie()
    rive: true

  assets:
    enabled: true
    outputs:
      class_name: Assets
      style: dot-delimiter   # Assets.images.logo
      package_parameter_enabled: false

  fonts:
    enabled: true

  colors:
    inputs:
      - assets/colors/colors.xml

Per generare il codice:

dart run build_runner build --delete-conflicting-outputs

In alternativa si può installare la CLI globale, utile quando non si vuole far girare l'intera pipeline di build_runner:

dart pub global activate flutter_gen
fluttergen -c pubspec.yaml

Il risultato sono file come lib/gen/assets.gen.dart, lib/gen/fonts.gen.dart e lib/gen/colors.gen.dart.

Usare gli asset generati

Immagini

Ogni immagine diventa un oggetto AssetGenImage con metodi pronti all'uso:

import 'package:myapp/gen/assets.gen.dart';

// Il path grezzo, se serve
final path = Assets.images.logo.path; // 'assets/images/logo.png'

// Widget Image già configurato
Assets.images.logo.image(
  width: 120,
  fit: BoxFit.contain,
  filterQuality: FilterQuality.medium,
);

// ImageProvider, utile per DecorationImage o precaching
Container(
  decoration: BoxDecoration(
    image: DecorationImage(
      image: Assets.images.background.provider(),
      fit: BoxFit.cover,
    ),
  ),
);

Le varianti di risoluzione (2.0x/, 3.0x/) continuano a funzionare esattamente come prima: flutter_gen genera un solo simbolo per l'immagine base, mentre Flutter sceglie a runtime la variante corretta in base al devicePixelRatio.

SVG

Con flutter_svg: true nelle integrazioni, i file .svg espongono un metodo dedicato:

Assets.icons.menu.svg(
  width: 24,
  height: 24,
  colorFilter: ColorFilter.mode(
    Theme.of(context).colorScheme.onSurface,
    BlendMode.srcIn,
  ),
);

Il pacchetto flutter_svg deve ovviamente essere tra le dipendenze del progetto: flutter_gen si limita a generare la chiamata.

Lottie, Rive e file generici

// Animazione Lottie
Assets.lottie.loading.lottie(repeat: true);

// File JSON o qualsiasi altro asset: resta una String tipizzata
final raw = await rootBundle.loadString(Assets.json.remoteConfigDefaults);

Font

import 'package:myapp/gen/fonts.gen.dart';

TextStyle(
  fontFamily: FontFamily.montserrat,
  fontWeight: FontWeight.w700,
);

Se un giorno il designer cambia font e il family nel pubspec diventa Inter, la rigenerazione fa esplodere a compile time tutti i punti che usavano FontFamily.montserrat. È esattamente quello che vogliamo.

Colori da XML

Se condividi la palette con il team Android (o la esporti da Figma in formato XML), flutter_gen può generarla:

<!-- assets/colors/colors.xml -->
<?xml version="1.0" encoding="utf-8"?>
<resources>
  <color name="primary">#FF6750A4</color>
  <color name="surface_dim">#FFDED8E1</color>
</resources>
import 'package:myapp/gen/colors.gen.dart';

ColoredBox(color: ColorName.primary);

Nota: per un design system completo restano preferibili ThemeExtension e ColorScheme; ColorName è comodo come sorgente di valori grezzi da iniettare nel tema, non come sostituto del tema stesso.

Un caso d'uso concreto: precaching delle immagini

Una delle funzioni meno note è la lista values, che raccoglie tutti gli asset di una cartella. Diventa banale fare il precache delle immagini critiche dello splash o dell'onboarding:

Future<void> precacheOnboarding(BuildContext context) async {
  await Future.wait([
    for (final image in Assets.images.onboarding.values)
      precacheImage(image.provider(), context),
  ]);
}

Se domani il designer aggiunge una quarta schermata di onboarding, il file viene incluso automaticamente: nessuna lista da aggiornare a mano.

Organizzare le cartelle per avere nomi leggibili

I nomi generati derivano direttamente dal filesystem, quindi la struttura delle cartelle è la tua API. Qualche regola pratica:

  • usa snake_case per i file: profile_placeholder.png diventa profilePlaceholder, mentre Profile Placeholder.png genera nomi poco gradevoli (gli spazi vengono normalizzati, ma meglio evitarli);
  • non iniziare i nomi con un numero: 1_step.png non è un identificatore Dart valido e viene prefissato automaticamente;
  • raggruppa per dominio, non per tipo: assets/images/onboarding/, assets/images/empty_states/ sono più utili di una cartella images piatta con 80 file;
  • attenzione ai duplicati: logo.png e logo.svg nella stessa cartella generano collisioni di nome. Rinomina uno dei due (logo_mark.svg).

Se preferisci nomi "piatti" puoi cambiare lo style in snake-case o camel-case: otterrai Assets.imagesOnboardingStepOne invece di Assets.images.onboarding.stepOne. Nella maggior parte dei casi dot-delimiter è più leggibile.

Monorepo e package condivisi

Se gli asset vivono in un package separato (scenario comune con Melos), attiva:

flutter_gen:
  assets:
    outputs:
      package_parameter_enabled: true

Il codice generato includerà automaticamente il parametro package:, evitando il classico errore "asset non trovato" quando si carica una risorsa da un package diverso da quello dell'app host.

Generated code e controllo di versione

La domanda ricorrente: committare o no i file in lib/gen/?

Committarli è la scelta più pragmatica per i team piccoli: l'IDE funziona subito dopo il clone, le PR mostrano esplicitamente quando un asset viene aggiunto o rimosso e la CI non deve eseguire un passo extra prima dell'analisi statica.

Se invece preferisci tenerli fuori dal repository, aggiungi a .gitignore la cartella di output e inserisci la generazione nella pipeline prima di flutter analyze:

- run: flutter pub get
- run: dart run build_runner build --delete-conflicting-outputs
- run: flutter analyze
- run: flutter test

In entrambi i casi conviene escludere il codice generato dall'analisi statica e dal coverage:

# analysis_options.yaml
analyzer:
  exclude:
    - lib/gen/**
    - "**/*.g.dart"

Cosa flutter_gen non fa

È importante conoscere i limiti dello strumento:

  • non ottimizza gli asset: comprimere i PNG, convertire in WebP o ripulire gli SVG resta compito tuo (o di un passo di build dedicato);
  • non verifica a runtime: se cancelli un file e dimentichi di rigenerare, il codice compila ancora e fallisce a runtime. Per questo vale la pena aggiungere un check in CI che rigeneri e confronti il diff (git diff --exit-code lib/gen/);
  • non sostituisce la localizzazione: per le stringhe servono intl o slang;
  • non riduce il peso del bundle: se un asset è dichiarato nel pubspec viene incluso, anche se nessuno lo referenzia. Un vantaggio indiretto però c'è: con i simboli generati è molto più facile cercare le referenze e capire quali immagini sono davvero inutilizzate prima di cancellarle.

Workflow consigliato

  1. Aggiungi l'asset nella cartella corretta con un nome in snake_case.
  2. Verifica che la cartella sia dichiarata in flutter: assets: (basta la directory, non il singolo file).
  3. Rigenera con dart run build_runner build --delete-conflicting-outputs — oppure tieni attivo watch durante lo sviluppo:
dart run build_runner watch --delete-conflicting-outputs
  1. Usa il simbolo generato e vieta per convenzione (anche via code review) qualsiasi Image.asset('...') con stringa letterale.

Per rendere la regola effettiva puoi persino aggiungere un lint custom con custom_lint che segnala l'uso di Image.asset con argomento letterale, ma nella pratica è sufficiente che il team adotti l'abitudine: una volta provato l'autocompletamento su Assets., tornare indietro è difficile.

Conclusione

flutter_gen è uno di quegli strumenti che costano cinque minuti di setup e restituiscono valore per tutta la vita del progetto. Trasforma risorse fragili — path, nomi di font, palette — in simboli Dart verificati dal compilatore, elimina un'intera categoria di bug a runtime e rende i refactoring degli asset un'operazione meccanica invece che un esercizio di archeologia tra le stringhe.