Testo, immagini, icone e asset in Flutter: configurare il pubspec.yaml

Foto di 2H Media su Unsplash

GuidePrincipiante35 min Flutter 3.x

Testo, immagini, icone e asset in Flutter: configurare il pubspec.yaml

Ogni app ha bisogno di contenuti: parole, immagini, icone e magari un font personalizzato. In Flutter tutto questo passa da tre elementi fondamentali: i widget Text, Image e Icon, e il file di configurazione pubspec.yaml.

Il pubspec.yaml è il "documento d'identità" del progetto: dichiara nome, dipendenze e — cosa che interessa a noi in questo tutorial — le risorse statiche (asset) che vogliamo includere nel bundle dell'app. Se dimentichi di dichiarare un'immagine lì dentro, Flutter non la troverà a runtime e ti mostrerà il classico errore Unable to load asset.

In questa guida costruiremo passo dopo passo una piccola schermata "profilo prodotto" che mette insieme:

  • testi formattati con TextStyle e Theme.of(context).textTheme;
  • un'immagine locale caricata dalla cartella assets/;
  • un'immagine di rete con placeholder e gestione dell'errore;
  • icone Material e un font personalizzato dichiarato nel pubspec.yaml.

Non serve alcuna conoscenza avanzata: basta avere Flutter installato e un progetto creato con flutter create.

  1. 1

    Preparare il progetto e la cartella degli asset

    Creiamo un nuovo progetto (o apriamone uno esistente) e organizziamo le risorse in cartelle dedicate. La convenzione più diffusa prevede una cartella assets/ nella root del progetto, con sottocartelle per tipologia.

    flutter create asset_demo
    cd asset_demo
    

    Crea questa struttura accanto a lib/ e pubspec.yaml:

    asset_demo/
    ├── assets/
    │   ├── images/
    │   │   └── prodotto.png
    │   └── fonts/
    │       ├── Poppins-Regular.ttf
    │       └── Poppins-Bold.ttf
    ├── lib/
    └── pubspec.yaml
    

    Per l'immagine puoi usare un qualsiasi PNG/JPG (rinominalo prodotto.png). Per il font puoi scaricare gratuitamente i file .ttf di Poppins da Google Fonts.

    Nota: i nomi dei file asset devono essere in minuscolo, senza spazi né accenti. Su alcune piattaforme il filesystem è case-sensitive e Prodotto.PNG non verrebbe trovato.

    # Comandi da terminale
    flutter create asset_demo
    cd asset_demo
    mkdir -p assets/images assets/fonts

    Risultato atteso

    Il progetto compila con `flutter run` e nella root esiste la cartella `assets/` con le sottocartelle `images/` e `fonts/`.

  2. 2

    Dichiarare asset e font nel pubspec.yaml

    Flutter include nel bundle finale solo i file dichiarati esplicitamente. Apriamo pubspec.yaml e cerchiamo la sezione flutter: (attenzione: quella in fondo al file, non dependencies).

    L'indentazione in YAML è fondamentale: si usano spazi, mai tab, e ogni livello vale due spazi.

    Due modi per dichiarare le immagini:

    1. File per file: - assets/images/prodotto.png
    2. Intera cartella: - assets/images/ (con la barra finale). Include tutti i file della cartella, ma non delle sottocartelle: queste vanno dichiarate a parte.

    Per i font si usa la chiave fonts, con family (il nome che userai nel codice) e la lista degli asset con il relativo weight.

    name: asset_demo
    description: Demo su asset, testo e immagini
    publish_to: 'none'
    version: 1.0.0+1
    
    environment:
      sdk: '>=3.0.0 <4.0.0'
    
    dependencies:
      flutter:
        sdk: flutter
      cupertino_icons: ^1.0.6
    
    dev_dependencies:
      flutter_test:
        sdk: flutter
      flutter_lints: ^3.0.0
    
    flutter:
      uses-material-design: true
    
      assets:
        - assets/images/
    
      fonts:
        - family: Poppins
          fonts:
            - asset: assets/fonts/Poppins-Regular.ttf
              weight: 400
            - asset: assets/fonts/Poppins-Bold.ttf
              weight: 700

    Risultato atteso

    Dopo il salvataggio, l'IDE esegue `flutter pub get` senza errori di parsing YAML. Se hai modificato il pubspec, ferma e riavvia l'app: l'hot reload non basta per i nuovi asset.

  3. 3

    Mostrare del testo con Text e TextStyle

    Il widget Text è il modo base per scrivere sullo schermo. Accetta una stringa e, opzionalmente, uno style di tipo TextStyle.

    Proprietà utili di TextStyle:

    • fontSize, fontWeight, color, letterSpacing;
    • fontFamily: qui useremo 'Poppins', esattamente come scritto in family nel pubspec;
    • height: moltiplicatore dell'interlinea.

    Su Text invece trovi:

    • maxLines e overflow: TextOverflow.ellipsis per troncare testi lunghi con i tre puntini;
    • textAlign per l'allineamento.

    Una buona pratica è però non scrivere stili a mano ovunque, ma partire dal tema: Theme.of(context).textTheme.titleLarge restituisce uno stile coerente con il resto dell'app, che puoi personalizzare con copyWith().

    Impostando fontFamily: 'Poppins' in ThemeData, tutti i testi dell'app useranno il font personalizzato senza ripeterlo widget per widget.

    import 'package:flutter/material.dart';
    
    void main() => runApp(const MyApp());
    
    class MyApp extends StatelessWidget {
      const MyApp({super.key});
    
      @override
      Widget build(BuildContext context) {
        return MaterialApp(
          title: 'Asset Demo',
          theme: ThemeData(
            colorSchemeSeed: Colors.teal,
            fontFamily: 'Poppins', // font dichiarato nel pubspec
            useMaterial3: true,
          ),
          home: const ProdottoPage(),
        );
      }
    }
    
    class ProdottoPage extends StatelessWidget {
      const ProdottoPage({super.key});
    
      @override
      Widget build(BuildContext context) {
        final textTheme = Theme.of(context).textTheme;
    
        return Scaffold(
          appBar: AppBar(title: const Text('Scheda prodotto')),
          body: Padding(
            padding: const EdgeInsets.all(16),
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                Text('Zaino da trekking 30L', style: textTheme.headlineSmall),
                const SizedBox(height: 4),
                Text(
                  'Leggero, impermeabile e con schienale traspirante. '
                  'Perfetto per escursioni in giornata.',
                  style: textTheme.bodyMedium?.copyWith(
                    color: Colors.grey.shade700,
                    height: 1.4,
                  ),
                  maxLines: 3,
                  overflow: TextOverflow.ellipsis,
                ),
              ],
            ),
          ),
        );
      }
    }

    Risultato atteso

    L'app mostra un titolo e una descrizione su tre righe al massimo, resi con il font Poppins.

  4. 4

    Caricare un'immagine locale con Image.asset

    Per mostrare un file dichiarato nel pubspec si usa Image.asset('percorso/relativo/dalla/root'). Il percorso è identico a quello scritto nel pubspec, quindi assets/images/prodotto.png.

    Parametri importanti:

    • width / height: dimensioni in logical pixel;
    • fit: come l'immagine riempie lo spazio. BoxFit.cover riempie tutto ritagliando l'eccesso, BoxFit.contain mostra tutto lasciando eventuali spazi vuoti;
    • errorBuilder: widget alternativo se il caricamento fallisce.

    Per angoli arrotondati avvolgi l'immagine in ClipRRect. Se invece l'immagine serve come sfondo di un contenitore, usa Container(decoration: BoxDecoration(image: DecorationImage(...))).

    Immagini a risoluzione multipla

    Flutter supporta le varianti per densità di schermo: basta creare le sottocartelle 2.0x e 3.0x accanto al file base. Dichiari solo il percorso base e il framework sceglie la variante giusta.

    assets/images/prodotto.png
    assets/images/2.0x/prodotto.png
    assets/images/3.0x/prodotto.png
    
    ClipRRect(
      borderRadius: BorderRadius.circular(16),
      child: Image.asset(
        'assets/images/prodotto.png',
        width: double.infinity,
        height: 200,
        fit: BoxFit.cover,
        errorBuilder: (context, error, stackTrace) => Container(
          height: 200,
          color: Colors.grey.shade200,
          alignment: Alignment.center,
          child: const Icon(Icons.broken_image_outlined, size: 48),
        ),
      ),
    )

    Risultato atteso

    L'immagine locale appare con angoli arrotondati e altezza 200. Se il percorso è sbagliato compare l'icona di fallback invece di un crash.

  5. 5

    Immagini da internet con Image.network e stato di caricamento

    Per le immagini remote si usa Image.network(url). A differenza degli asset, il download è asincrono: conviene sempre gestire i due casi limite.

    • loadingBuilder: viene chiamato mentre l'immagine scarica; riceve ImageChunkEvent con i byte ricevuti e totali, utili per una progress bar reale.
    • errorBuilder: mostrato in caso di URL non valido o assenza di rete.

    Attenzione: su Android, dalla versione 9 in poi, il traffico HTTP in chiaro è bloccato. Usa sempre URL https. Su macOS e Windows, per le app desktop, ricorda di abilitare il permesso di rete nelle entitlement.

    Se l'app carica molte immagini remote in liste, valuta più avanti pacchetti dedicati alla cache; per ora Image.network è più che sufficiente.

    Image.network(
      'https://picsum.photos/600/400',
      width: double.infinity,
      height: 200,
      fit: BoxFit.cover,
      loadingBuilder: (context, child, progress) {
        if (progress == null) return child; // download completato
        final total = progress.expectedTotalBytes;
        return SizedBox(
          height: 200,
          child: Center(
            child: CircularProgressIndicator(
              value: total != null
                  ? progress.cumulativeBytesLoaded / total
                  : null,
            ),
          ),
        );
      },
      errorBuilder: (context, error, stackTrace) => const SizedBox(
        height: 200,
        child: Center(child: Text('Immagine non disponibile')),
      ),
    )

    Risultato atteso

    Durante il download compare un indicatore circolare, poi l'immagine remota. Senza connessione appare il messaggio di errore.

  6. 6

    Icone Material, icone personalizzate e Icon Button

    Flutter include gratuitamente l'intero set Material Icons, disponibile grazie alla riga uses-material-design: true nel pubspec (se la rimuovi, le icone non vengono renderizzate).

    Si usano così:

    • Icon(Icons.star, size: 20, color: Colors.amber) per un'icona statica;
    • IconButton(onPressed: ..., icon: Icon(...)) per un'icona cliccabile con effetto ripple;
    • ImageIcon(AssetImage('assets/images/logo.png')) se vuoi usare un PNG monocromatico come icona, colorabile con color.

    Per i loghi vettoriali dei brand, di solito si aggiunge il pacchetto flutter_svg e si usa SvgPicture.asset, ricordando sempre di dichiarare il file .svg tra gli asset.

    Mettiamo insieme icone, testo e immagini in una riga di valutazione del prodotto.

    Row(
      children: [
        ...List.generate(
          5,
          (i) => Icon(
            i < 4 ? Icons.star_rounded : Icons.star_border_rounded,
            size: 22,
            color: Colors.amber.shade700,
          ),
        ),
        const SizedBox(width: 8),
        const Text('4.0 (128 recensioni)'),
        const Spacer(),
        IconButton(
          tooltip: 'Aggiungi ai preferiti',
          onPressed: () {
            ScaffoldMessenger.of(context).showSnackBar(
              const SnackBar(content: Text('Aggiunto ai preferiti')),
            );
          },
          icon: const Icon(Icons.favorite_border),
        ),
      ],
    )

    Risultato atteso

    Compare una riga con quattro stelle piene, una vuota, il testo delle recensioni e un pulsante cuore che mostra uno SnackBar al tocco.

  7. 7

    Assemblare la schermata completa ed evitare gli errori tipici

    Uniamo tutti i pezzi in un'unica schermata scrollabile. Usiamo SingleChildScrollView per evitare il famigerato overflow giallo/nero quando la tastiera o uno schermo piccolo riducono lo spazio.

    Checklist degli errori più comuni

    1. Unable to load asset: assets/images/prodotto.png → il file non è dichiarato nel pubspec, il percorso ha un refuso, oppure l'indentazione YAML è sbagliata.
    2. Ho aggiunto l'asset ma non si vede → l'hot reload non ricostruisce il bundle: ferma l'app e rilanciala con flutter run (full restart).
    3. Il font non cambia → il valore di fontFamily nel codice deve coincidere esattamente con family nel pubspec (maiuscole comprese).
    4. RenderFlex overflowed by N pixels → un Column troppo alto: avvolgilo in SingleChildScrollView o usa Expanded/Flexible per i figli elastici.
    5. Immagine deformata → stai forzando width e height senza fit: aggiungi BoxFit.cover o BoxFit.contain.
    6. App pesantissima → PNG da 4000px per un'anteprima da 100px. Ridimensiona e comprimi le immagini prima di includerle.
    class ProdottoPage extends StatelessWidget {
      const ProdottoPage({super.key});
    
      @override
      Widget build(BuildContext context) {
        final textTheme = Theme.of(context).textTheme;
    
        return Scaffold(
          appBar: AppBar(title: const Text('Scheda prodotto')),
          body: SingleChildScrollView(
            padding: const EdgeInsets.all(16),
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                ClipRRect(
                  borderRadius: BorderRadius.circular(16),
                  child: Image.asset(
                    'assets/images/prodotto.png',
                    width: double.infinity,
                    height: 220,
                    fit: BoxFit.cover,
                    errorBuilder: (_, __, ___) => Container(
                      height: 220,
                      color: Colors.grey.shade200,
                      alignment: Alignment.center,
                      child: const Icon(Icons.broken_image_outlined, size: 48),
                    ),
                  ),
                ),
                const SizedBox(height: 16),
                Text('Zaino da trekking 30L', style: textTheme.headlineSmall),
                const SizedBox(height: 4),
                Text(
                  '89,90 €',
                  style: textTheme.titleMedium?.copyWith(
                    color: Theme.of(context).colorScheme.primary,
                    fontWeight: FontWeight.w700,
                  ),
                ),
                const SizedBox(height: 12),
                Row(
                  children: [
                    ...List.generate(
                      5,
                      (i) => Icon(
                        i < 4 ? Icons.star_rounded : Icons.star_border_rounded,
                        size: 22,
                        color: Colors.amber.shade700,
                      ),
                    ),
                    const SizedBox(width: 8),
                    Text('4.0 (128)', style: textTheme.bodySmall),
                  ],
                ),
                const SizedBox(height: 16),
                Text(
                  'Leggero, impermeabile e con schienale traspirante. '
                  'Perfetto per escursioni in giornata.',
                  style: textTheme.bodyMedium?.copyWith(height: 1.5),
                ),
                const SizedBox(height: 24),
                Text('Galleria', style: textTheme.titleMedium),
                const SizedBox(height: 8),
                ClipRRect(
                  borderRadius: BorderRadius.circular(12),
                  child: Image.network(
                    'https://picsum.photos/600/400',
                    width: double.infinity,
                    height: 180,
                    fit: BoxFit.cover,
                    loadingBuilder: (context, child, progress) =>
                        progress == null
                            ? child
                            : const SizedBox(
                                height: 180,
                                child: Center(child: CircularProgressIndicator()),
                              ),
                    errorBuilder: (_, __, ___) => const SizedBox(
                      height: 180,
                      child: Center(child: Text('Immagine non disponibile')),
                    ),
                  ),
                ),
                const SizedBox(height: 24),
                SizedBox(
                  width: double.infinity,
                  child: FilledButton.icon(
                    onPressed: () {},
                    icon: const Icon(Icons.shopping_cart_outlined),
                    label: const Text('Aggiungi al carrello'),
                  ),
                ),
              ],
            ),
          ),
        );
      }
    }

    Risultato atteso

    Una schermata prodotto scrollabile con immagine locale, prezzo, stelle, descrizione con font Poppins, immagine remota e pulsante d'acquisto, senza errori di overflow.

CondividiXLinkedInFacebookWhatsApp

Commenti (0)

Ancora nessun commento. Inizia tu!