Perché parlare di SVG in Flutter

A differenza del web o di Android (che ha i VectorDrawable), il framework Flutter non sa disegnare un file SVG nativamente: Image.asset() accetta PNG, JPEG, WebP, GIF e BMP, ma non XML vettoriale. Eppure gli asset vettoriali sono spesso la scelta migliore: un solo file scala perfettamente su ogni densità di schermo, pesa pochi KB e può essere ricolorato a runtime per seguire il tema dell'app.

La soluzione di riferimento è il pacchetto flutter_svg, che dalla versione 2.x è stato riscritto sopra vector_graphics, il motore ufficiale mantenuto dal team Flutter. Vediamo come usarlo bene, quali sono i suoi limiti e come spremerne le performance con la precompilazione degli asset.

Installazione e primo utilizzo

dependencies:
  flutter_svg: ^2.0.10
  vector_graphics: ^1.1.11

dev_dependencies:
  vector_graphics_compiler: ^1.1.11

Ricorda di dichiarare la cartella degli asset nel pubspec.yaml:

flutter:
  assets:
    - assets/icons/

Il widget base è SvgPicture, disponibile in quattro costruttori (asset, network, file, string):

import 'package:flutter_svg/flutter_svg.dart';

SvgPicture.asset(
  'assets/icons/logo.svg',
  width: 120,
  height: 120,
  fit: BoxFit.contain,
  semanticsLabel: 'Logo aziendale',
);

A differenza di Image, SvgPicture non ha una dimensione intrinseca garantita: se non specifichi width/height il widget prova a occupare lo spazio concesso dal parent. Dentro una Row o una Column senza vincoli è la prima causa di layout "che esplodono": conviene sempre wrappare in un SizedBox o passare dimensioni esplicite.

Ricolorare un SVG in base al tema

Nelle vecchie versioni si usava il parametro color, oggi deprecato in favore di colorFilter, che è più esplicito sul BlendMode applicato:

SvgPicture.asset(
  'assets/icons/heart.svg',
  width: 24,
  colorFilter: ColorFilter.mode(
    Theme.of(context).colorScheme.primary,
    BlendMode.srcIn,
  ),
);

BlendMode.srcIn sostituisce tutti i colori del disegno mantenendo l'alpha: è quello che serve nel 95% dei casi per le icone monocromatiche. Se invece l'SVG è multicolore (un'illustrazione, un logo) non applicare filtri.

Un pattern comodo è incapsulare tutto in un widget riutilizzabile, così da non ripetere la stessa configurazione in decine di punti:

class AppIcon extends StatelessWidget {
  const AppIcon(this.name, {super.key, this.size = 24, this.color});

  final String name;
  final double size;
  final Color? color;

  @override
  Widget build(BuildContext context) {
    final tint = color ?? IconTheme.of(context).color;
    return SvgPicture.asset(
      'assets/icons/$name.svg',
      width: size,
      height: size,
      colorFilter: tint == null
          ? null
          : ColorFilter.mode(tint, BlendMode.srcIn),
    );
  }
}

Così AppIcon('heart') eredita automaticamente il colore dell'IconTheme corrente, esattamente come farebbe un Icon di Material.

Placeholder, errori e SVG remoti

Gli SVG caricati da rete richiedono un attimo per arrivare e vanno gestiti come qualsiasi risorsa asincrona:

SvgPicture.network(
  'https://example.com/badge.svg',
  width: 64,
  placeholderBuilder: (context) => const SizedBox(
    width: 64,
    height: 64,
    child: Center(child: CircularProgressIndicator(strokeWidth: 2)),
  ),
);

Attenzione: SvgPicture.network mantiene una cache in memoria ma non una cache su disco. Se gli SVG remoti sono centrali nella tua UI, scaricali con Dio/http e passali a SvgPicture.memory, oppure usa un BytesLoader personalizzato che legge da una cache su file system.

Precompilare gli SVG con vector_graphics

Qui arriva la parte interessante in termini di performance. Quando carichi un .svg, a runtime l'app deve: leggere l'asset, fare il parsing dell'XML, risolvere gradienti/trasformazioni/path e costruire la lista di comandi di disegno. Su icone semplici è irrilevante, su illustrazioni complesse o su liste con molti elementi diventa jank visibile.

vector_graphics_compiler sposta tutto questo lavoro al build time, producendo un formato binario .vec già ottimizzato (path semplificati, mask risolte, comandi pronti per il canvas).

# singolo file
dart run vector_graphics_compiler -i assets/icons/logo.svg -o assets/vec/logo.vec

# batch su una cartella
for f in assets/icons/*.svg; do
  name=$(basename "$f" .svg)
  dart run vector_graphics_compiler -i "$f" -o "assets/vec/$name.vec"
done

Aggiungi assets/vec/ al pubspec.yaml e carica i file compilati con un BytesLoader:

import 'package:vector_graphics/vector_graphics.dart';

const logoLoader = AssetBytesLoader('assets/vec/logo.vec');

VectorGraphic(
  loader: logoLoader,
  width: 120,
  colorFilter: const ColorFilter.mode(Colors.indigo, BlendMode.srcIn),
);

Puoi anche continuare a usare SvgPicture, che accetta direttamente un loader:

SvgPicture(logoLoader, width: 120);

Un consiglio pratico: integra la compilazione nella pipeline CI (o in uno script tool/build_icons.sh) e committa i .vec, oppure genera i file in fase di build e tienili fuori dal repository. L'importante è che il team non debba ricordarsi di lanciare il comando a mano.

Precaching: evitare il "lampo" al primo frame

Anche con i .vec, il primo caricamento di un asset avviene in modo asincrono e per un frame vedi lo spazio vuoto. Per le schermate critiche (splash, onboarding, home) conviene fare precaching:

Future<void> precacheVectors() async {
  const loaders = <BytesLoader>[
    AssetBytesLoader('assets/vec/logo.vec'),
    AssetBytesLoader('assets/vec/empty_state.vec'),
  ];

  for (final loader in loaders) {
    await svg.cache.putIfAbsent(
      loader.cacheKey(null),
      () => loader.loadBytes(null),
    );
  }
}

Chiamalo dopo WidgetsFlutterBinding.ensureInitialized() in main(), oppure in didChangeDependencies della schermata precedente.

Cosa non funziona: i limiti da conoscere

flutter_svg supporta un sottoinsieme ampio ma non completo della specifica SVG. In particolare non sono supportati (o lo sono solo parzialmente):

  • animazioni SMIL (<animate>, <animateTransform>);
  • script e CSS esterni;
  • la maggior parte dei filtri (<filter>, blur gaussiani complessi);
  • l'elemento <text>, che va convertito in path prima dell'export.

La regola d'oro è: esporta sempre l'SVG con testo convertito in tracciati e fai passare i file da un ottimizzatore come SVGO prima di importarli. Un SVG esportato "grezzo" da Figma o Illustrator può contenere migliaia di nodi inutili e gruppi annidati che pesano sul rendering. Se il compiler segnala warning, è il momento di semplificare il file a monte, non di combattere con il codice Dart.

Il resto degli asset: densità, tree-shaking e type-safety

Gli SVG non risolvono tutto: foto e texture restano raster. Per quelli, ricorda le asset variants basate sulla densità dello schermo:

assets/images/hero.png        # 1x
assets/images/2.0x/hero.png
assets/images/3.0x/hero.png

Nel pubspec.yaml dichiari solo assets/images/hero.png: Flutter sceglie la variante giusta in base al devicePixelRatio. E quando mostri un'immagine molto più grande dello spazio in cui verrà disegnata, usa cacheWidth/cacheHeight per decodificarla già ridimensionata e risparmiare memoria:

Image.asset('assets/images/hero.png', cacheWidth: 800);

Per le icone dei font (Material Icons, Cupertino Icons) Flutter applica automaticamente il tree-shaking: nelle build di release include solo i glifi effettivamente usati, purché tu non costruisca IconData dinamicamente da un code point.

Infine, per eliminare le stringhe magiche sui percorsi, vale la pena aggiungere flutter_gen:

dev_dependencies:
  build_runner: ^2.4.0
  flutter_gen_runner: ^5.4.0

flutter_gen:
  integrations:
    flutter_svg: true

Dopo dart run build_runner build puoi scrivere:

Assets.icons.heart.svg(width: 24);
Assets.images.hero.image(fit: BoxFit.cover);

Niente più refusi nei path e un errore di compilazione — invece di un crash a runtime — se rinomini o cancelli un file.

Checklist finale

  • Usa SVG per icone e illustrazioni, raster solo per foto e texture.
  • Ricolora con colorFilter + BlendMode.srcIn, mai con il deprecato color.
  • Dai sempre vincoli di dimensione a SvgPicture.
  • Precompila in .vec con vector_graphics_compiler gli asset complessi o usati in liste.
  • Fai precaching degli asset visibili nei primi frame.
  • Ottimizza i file a monte (SVGO, testo convertito in path).
  • Genera i path con flutter_gen per avere asset type-safe.

Con queste accortezze la grafica vettoriale in Flutter diventa un vantaggio netto: meno MB nel bundle, nessuna icona sgranata su nessun display e una UI che cambia colore insieme al tema senza duplicare un solo file.