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 deprecatocolor. - Dai sempre vincoli di dimensione a
SvgPicture. - Precompila in
.vecconvector_graphics_compilergli 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_genper 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.