Perché icona e splash non sono un dettaglio
Quando pubblichiamo un'app Flutter, l'utente vede due cose prima ancora del nostro primo widget: l'icona nel launcher e la schermata di avvio nativa. Sono elementi che vivono fuori dal framework Flutter: risiedono nelle cartelle android/ e ios/, sono gestiti dal sistema operativo e richiedono decine di asset in risoluzioni diverse.
Generarli a mano significa produrre manualmente mipmap-hdpi, mipmap-xxxhdpi, Assets.xcassets/AppIcon.appiconset con una ventina di PNG, più i file per web, Windows e macOS. Due pacchetti risolvono il problema in pochi secondi: flutter_launcher_icons e flutter_native_splash.
In questa guida vediamo la configurazione completa, incluse le parti che spesso vengono dimenticate: le adaptive icons, l'icona monocromatica per i temi dinamici di Android 13+, le API di splash screen introdotte da Android 12 e la gestione dei flavor.
Preparare gli asset sorgente
Prima di configurare qualsiasi cosa, servono immagini sorgente di buona qualità:
- Icona: un PNG quadrato di almeno 1024x1024 px, senza angoli arrotondati (li applica il sistema) e senza canale alpha per iOS.
- Foreground adattivo Android: un PNG 1024x1024 in cui il contenuto visibile occupa al massimo il 66% centrale. Android ritaglia l'icona in forme diverse (cerchio, squircle, goccia) e tutto ciò che esce dalla safe zone viene tagliato.
- Splash: un logo PNG con sfondo trasparente, tipicamente 1152x1152 px per la versione Android 12+.
Una struttura tipica del progetto:
assets/
branding/
icon.png # 1024x1024, opaco
icon_foreground.png # 1024x1024, trasparente, safe zone 66%
icon_monochrome.png # 1024x1024, silhouette bianca su trasparente
splash_logo.png
splash_logo_dark.png
flutter_launcher_icons: generare le icone
Aggiungiamo il pacchetto tra le dipendenze di sviluppo:
dev_dependencies:
flutter_launcher_icons: ^0.14.1
La configurazione può stare direttamente in pubspec.yaml oppure, come consiglio, in un file separato flutter_launcher_icons.yaml per non appesantire il pubspec:
# flutter_launcher_icons.yaml
flutter_launcher_icons:
image_path: "assets/branding/icon.png"
android: "ic_launcher"
adaptive_icon_background: "#0B1F3A"
adaptive_icon_foreground: "assets/branding/icon_foreground.png"
adaptive_icon_monochrome: "assets/branding/icon_monochrome.png"
min_sdk_android: 23
ios: true
remove_alpha_ios: true
background_color_ios: "#0B1F3A"
web:
generate: true
image_path: "assets/branding/icon.png"
background_color: "#0B1F3A"
theme_color: "#0B1F3A"
windows:
generate: true
icon_size: 256
macos:
generate: true
Generazione:
dart run flutter_launcher_icons -f flutter_launcher_icons.yaml
Cosa succede sotto il cofano
- Su Android vengono creati i
mipmap-*e, se sono presentiadaptive_icon_*, i file XML inres/mipmap-anydpi-v26/ic_launcher.xmlche descrivono i layer background/foreground/monochrome. - Su iOS viene riempito
Runner/Assets.xcassets/AppIcon.appiconsetcon tutte le dimensioni richieste.remove_alpha_ios: trueè obbligatorio: App Store Connect rifiuta le icone con canale alpha. - Su web viene aggiornato anche
web/manifest.jsoncon le icone PWA.
L'icona monocromatica (Android 13+)
Con i themed icons, Android colora l'icona in base al wallpaper dell'utente usando il layer monochrome. Se non lo forniamo, il sistema mostra l'icona standard: nessun crash, ma l'app stona in una home screen tematizzata.
La regola per il file monocromatico: una silhouette piena, bianca su sfondo trasparente, senza gradienti né testo sottile. Anche qui vale la safe zone del 66%.
flutter_native_splash: la schermata di avvio
dev_dependencies:
flutter_native_splash: ^2.4.3
Da Android 12 la splash screen non è più un semplice windowBackground: il sistema usa la SplashScreen API, che mostra l'icona dell'app al centro di uno sfondo a tinta unita, con vincoli precisi sulle dimensioni. Per questo il pacchetto prevede una sezione dedicata android_12.
# flutter_native_splash.yaml
flutter_native_splash:
color: "#FFFFFF"
image: assets/branding/splash_logo.png
color_dark: "#0B1F3A"
image_dark: assets/branding/splash_logo_dark.png
android_12:
image: assets/branding/splash_logo.png
icon_background_color: "#FFFFFF"
image_dark: assets/branding/splash_logo_dark.png
icon_background_color_dark: "#0B1F3A"
android: true
ios: true
web: false
android_gravity: center
ios_content_mode: center
fullscreen: false
Generazione e, se serve, ripristino:
dart run flutter_native_splash:create --path=flutter_native_splash.yaml
# per rimuovere tutto ciò che è stato generato
dart run flutter_native_splash:remove
I vincoli di Android 12+
Questo è il punto in cui la maggior parte dei team sbaglia:
- L'immagine viene mostrata dentro un cerchio di 160 dp di diametro; con
icon_background_colorl'area utile del logo scende a circa 2/3 di quel cerchio. - Non esiste più il concetto di splash a schermo intero con immagine di sfondo: se passiamo un banner rettangolare, verrà ritagliato e apparirà sgranato.
- Il branding testuale va messo nell'app, non nella splash nativa.
La regola pratica: usare un logo semplice, centrato e vettorialmente pulito, e replicare eventuali elementi di brand nella prima schermata Flutter.
Tenere la splash visibile durante l'inizializzazione
Il problema classico: la splash nativa sparisce appena il motore Flutter è pronto, ma l'app deve ancora caricare token, preferenze o dati remoti. Il risultato è un flash bianco seguito da uno spinner.
Il pacchetto espone due API per evitarlo:
import 'package:flutter/widgets.dart';
import 'package:flutter_native_splash/flutter_native_splash.dart';
Future<void> main() async {
final binding = WidgetsFlutterBinding.ensureInitialized();
// Blocca la rimozione automatica della splash nativa.
FlutterNativeSplash.preserve(widgetsBinding: binding);
await _bootstrap();
runApp(const MyApp());
}
Future<void> _bootstrap() async {
// Esempio: preferenze, sessione utente, remote config...
await Future.wait([
SessionRepository.instance.restore(),
RemoteConfigService.instance.init(),
]);
}
La rimozione va fatta dopo il primo frame utile, non dentro main(), altrimenti si torna al flash iniziale:
class MyApp extends StatefulWidget {
const MyApp({super.key});
@override
State<MyApp> createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) {
FlutterNativeSplash.remove();
});
}
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Demo',
home: const HomePage(),
);
}
}
Attenzione: se
preserve()viene chiamato e per un errore (eccezione durante il bootstrap)remove()non viene mai invocato, l'app resta bloccata sulla splash. Conviene proteggere l'inizializzazione con untry/finallyo, meglio, rimuovere la splash in ogni caso e mostrare l'errore nell'interfaccia Flutter.
Future<void> main() async {
final binding = WidgetsFlutterBinding.ensureInitialized();
FlutterNativeSplash.preserve(widgetsBinding: binding);
BootstrapResult result;
try {
await _bootstrap();
result = BootstrapResult.ok();
} catch (e, s) {
result = BootstrapResult.error(e, s);
}
runApp(MyApp(bootstrap: result));
}
Una transizione fluida tra splash nativa e UI
Per evitare uno stacco brusco, un trucco efficace è replicare in Flutter la stessa composizione della splash nativa (stesso colore di sfondo, stesso logo, stessa posizione) e poi animare la transizione verso la home:
class SplashGate extends StatelessWidget {
const SplashGate({super.key, required this.ready, required this.child});
final bool ready;
final Widget child;
@override
Widget build(BuildContext context) {
return AnimatedSwitcher(
duration: const Duration(milliseconds: 350),
child: ready
? child
: const ColoredBox(
key: ValueKey('splash'),
color: Color(0xFFFFFFFF),
child: Center(
child: SizedBox(
width: 120,
height: 120,
child: Image(
image: AssetImage('assets/branding/splash_logo.png'),
),
),
),
),
);
}
}
L'utente percepisce una sola schermata continua invece di due passaggi distinti.
Gestire i flavor
Se l'app ha più ambienti (dev, staging, produzione) con icone diverse, entrambi i pacchetti supportano file di configurazione multipli.
Per le icone, il naming è convenzionale: flutter_launcher_icons-<flavor>.yaml.
flutter_launcher_icons-dev.yaml
flutter_launcher_icons-staging.yaml
flutter_launcher_icons-prod.yaml
dart run flutter_launcher_icons # genera tutti i flavor trovati
Per la splash, si passa esplicitamente il flavor:
dart run flutter_native_splash:create --flavor dev
dart run flutter_native_splash:create --flavor prod
Un banner "DEV" sull'icona di sviluppo evita l'errore classico di provare una build sbagliata credendola quella di produzione.
Automatizzare con uno script
Per non dover ricordare i comandi, conviene definirli una volta sola. Con melos o con un semplice script shell:
#!/usr/bin/env bash
set -e
echo "→ Generazione icone..."
dart run flutter_launcher_icons
echo "→ Generazione splash screen..."
dart run flutter_native_splash:create
echo "✓ Asset nativi aggiornati"
In alternativa, con melos:
# melos.yaml
scripts:
branding:
run: |
dart run flutter_launcher_icons && \
dart run flutter_native_splash:create
description: Rigenera icone e splash screen nativi
Va committato il codice generato?
Sì. I file generati finiscono in android/app/src/main/res e ios/Runner/Assets.xcassets, che sono cartelle sotto controllo di versione. Committarli garantisce che la CI produca build corrette senza dover eseguire i generatori a ogni pipeline. Il generatore va rilanciato solo quando cambiano gli asset di brand.
Problemi frequenti e soluzioni
| Sintomo | Causa probabile | Soluzione |
|---|---|---|
| Icona vecchia dopo la rigenerazione | Cache del launcher Android | Disinstallare l'app e reinstallare, oppure flutter clean |
| App Store rifiuta l'icona | Canale alpha presente | remove_alpha_ios: true e ricontrollo del PNG sorgente |
| Logo tagliato su Android 12+ | Immagine fuori dalla safe zone | Ridurre il contenuto a circa 2/3 del canvas |
| Splash bianca poi flash scuro | Mancata configurazione dark mode | Impostare color_dark e image_dark |
| App bloccata sulla splash | preserve() senza remove() |
Rimozione in addPostFrameCallback e gestione delle eccezioni |
| Icona adattiva con bordi strani | Foreground opaco a tutto schermo | Usare un PNG con sfondo trasparente |
Checklist finale prima della pubblicazione
- [ ] Icona testata su launcher circolare e squircle (Pixel e Samsung si comportano diversamente).
- [ ] Icona monocromatica verificata con i temi dinamici attivi su Android 13+.
- [ ] Splash controllata in light e dark mode.
- [ ] Nessun flash bianco tra splash nativa e primo frame Flutter.
- [ ] Icone generate anche per web/desktop se le piattaforme sono supportate.
- [ ] Flavor con icone distinguibili tra loro.
- [ ] Asset generati committati nel repository.
Conclusioni
Icone e splash screen sono fra le attività più rapide da automatizzare e fra le più trascurate. Con una configurazione di venti righe e due comandi otteniamo asset coerenti su tutte le piattaforme, conformi alle linee guida di Android 12+ e alle regole di validazione dell'App Store.
Il punto chiave da ricordare è che la splash nativa non è il posto giusto per il branding elaborato: serve a coprire i millisecondi di avvio del motore Flutter. Tutto il resto — animazioni, claim, transizioni — va costruito dentro l'app, dove abbiamo il pieno controllo del rendering.