[{"data":1,"prerenderedAt":85},["ShallowReactive",2],{"tutorial-transizioni-fluide-tra-schermate-in-flutter-con-il-widget-hero":3,"comments-tutorial-transizioni-fluide-tra-schermate-in-flutter-con-il-widget-hero":84},{"id":4,"title":5,"slug":6,"excerpt":7,"intro":8,"cover_image":9,"cover_remote_url":10,"cover_credit":11,"video_url":15,"video":16,"difficulty":20,"estimated_minutes":21,"flutter_version":22,"status":23,"published_at":24,"meta_title":25,"meta_description":26,"category":27,"author":31,"steps":33},85,"Transizioni fluide tra schermate in Flutter con il widget Hero","transizioni-fluide-tra-schermate-in-flutter-con-il-widget-hero","Impara a collegare due schermate con un'animazione condivisa usando il widget Hero: dalla card della lista al dettaglio, con immagini, testi e transizioni personalizzate.","Quando l'utente tocca l'immagine di un prodotto in una lista e si apre la schermata di dettaglio, un'app curata non \"salta\" bruscamente da una pagina all'altra: l'immagine **vola** dalla posizione iniziale a quella finale, guidando l'occhio e rendendo la navigazione naturale.\n\nIn Flutter questo effetto si ottiene con il widget `Hero`, ed è sorprendentemente semplice: non serve un `AnimationController`, non servono `Tween`. Basta avvolgere lo stesso elemento nelle due schermate con un `Hero` che condivide lo stesso `tag`, e il framework si occupa di calcolare posizione, dimensione e interpolazione durante la transizione del `Navigator`.\n\nIn questo tutorial costruiremo una piccola app con una lista di piatti: toccando una card si aprirà il dettaglio con un'animazione condivisa su immagine e titolo. Vedremo anche gli errori più comuni (tag duplicati, immagini che \"sfarfallano\") e come personalizzare la durata della transizione.\n\n> Prerequisiti: conoscere le basi di `Navigator.push`, `ListView` e `StatelessWidget`.","https:\u002F\u002Fflutter.it\u002Fstorage\u002Ftutorials\u002Fe509a350-6f8d-41c2-bfae-ad658b4256c7.jpg","https:\u002F\u002Fimages.unsplash.com\u002Fphoto-1763583022351-fd555ebb8c70?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w5NzA2NTJ8MHwxfHJhbmRvbXx8fHx8fHx8fDE3ODg1ODI2ODB8&ixlib=rb-4.1.0&q=80&w=1080",{"name":12,"author_url":13,"photo_url":14},"Liana S","https:\u002F\u002Funsplash.com\u002F@cherstve_pechivo","https:\u002F\u002Funsplash.com\u002Fphotos\u002Fflock-of-birds-flying-in-the-sky-at-sunset-rThefmQ89r8","https:\u002F\u002Fwww.youtube.com\u002Fwatch?v=JdZB_YinsGQ",{"youtube_id":17,"duration_seconds":18,"published_at":19},"JdZB_YinsGQ",136,"2026-09-05T08:42:34+00:00","beginner",30,"3.x","published","2026-09-05T04:31:20+00:00","Hero in Flutter: transizioni tra schermate","Guida beginner al widget Hero in Flutter: animazioni condivise tra lista e dettaglio, tag univoci, errori comuni e transizioni personalizzate.",{"id":28,"name":29,"slug":30},1,"Guide","guide",{"id":28,"name":32},"Flutter Bot",[34,42,49,56,63,70,77],{"id":35,"position":28,"title":36,"body":37,"code_snippet":38,"code_language":39,"expected_result":40,"demo_url":41,"video_url":41},566,"Preparare il progetto e il modello dati","Creiamo un nuovo progetto e definiamo un semplice modello `Dish` con id, nome, descrizione e URL dell'immagine. L'`id` ci servirà anche come **tag univoco** per gli Hero.\n\n```bash\nflutter create hero_demo\ncd hero_demo\n```\n\nPer caricare immagini di rete servono i permessi Internet (su Android sono già presenti in debug). Crea il file `lib\u002Fdish.dart` con il modello e una lista di dati finti.","\u002F\u002F lib\u002Fdish.dart\nclass Dish {\n  final String id;\n  final String name;\n  final String description;\n  final String imageUrl;\n\n  const Dish({\n    required this.id,\n    required this.name,\n    required this.description,\n    required this.imageUrl,\n  });\n}\n\nconst demoDishes = \u003CDish>[\n  Dish(\n    id: 'carbonara',\n    name: 'Carbonara',\n    description:\n        'Guanciale croccante, uovo, pecorino romano e pepe nero. Niente panna, mai.',\n    imageUrl: 'https:\u002F\u002Fpicsum.photos\u002Fid\u002F292\u002F800\u002F600',\n  ),\n  Dish(\n    id: 'margherita',\n    name: 'Pizza Margherita',\n    description:\n        'Pomodoro San Marzano, fiordilatte, basilico fresco e olio extravergine.',\n    imageUrl: 'https:\u002F\u002Fpicsum.photos\u002Fid\u002F1080\u002F800\u002F600',\n  ),\n  Dish(\n    id: 'tiramisu',\n    name: 'Tiramisù',\n    description:\n        'Savoiardi inzuppati nel caffè, crema al mascarpone e cacao amaro.',\n    imageUrl: 'https:\u002F\u002Fpicsum.photos\u002Fid\u002F431\u002F800\u002F600',\n  ),\n];","dart","Il progetto compila e hai un file `dish.dart` con tre piatti di esempio pronti da mostrare.",null,{"id":43,"position":44,"title":45,"body":46,"code_snippet":47,"code_language":39,"expected_result":48,"demo_url":41,"video_url":41},567,2,"Costruire la lista con le card dei piatti","Creiamo la schermata iniziale: una `ListView.builder` che mostra una card per ogni piatto, con immagine a sinistra e testo a destra. Per ora **senza** Hero: la aggiungeremo al passo successivo, così vedrai la differenza.\n\nNota l'uso di `InkWell` per rendere l'intera card toccabile e di `ClipRRect` per arrotondare gli angoli dell'immagine.","\u002F\u002F lib\u002Fmain.dart\nimport 'package:flutter\u002Fmaterial.dart';\nimport 'dish.dart';\nimport 'dish_detail_page.dart';\n\nvoid main() => runApp(const HeroDemoApp());\n\nclass HeroDemoApp extends StatelessWidget {\n  const HeroDemoApp({super.key});\n\n  @override\n  Widget build(BuildContext context) {\n    return MaterialApp(\n      title: 'Hero Demo',\n      theme: ThemeData(colorSchemeSeed: Colors.deepOrange, useMaterial3: true),\n      home: const DishListPage(),\n    );\n  }\n}\n\nclass DishListPage extends StatelessWidget {\n  const DishListPage({super.key});\n\n  @override\n  Widget build(BuildContext context) {\n    return Scaffold(\n      appBar: AppBar(title: const Text('Menù del giorno')),\n      body: ListView.builder(\n        padding: const EdgeInsets.all(12),\n        itemCount: demoDishes.length,\n        itemBuilder: (context, index) {\n          final dish = demoDishes[index];\n          return Card(\n            clipBehavior: Clip.antiAlias,\n            margin: const EdgeInsets.only(bottom: 12),\n            child: InkWell(\n              onTap: () {\n                Navigator.of(context).push(\n                  MaterialPageRoute(\n                    builder: (_) => DishDetailPage(dish: dish),\n                  ),\n                );\n              },\n              child: Padding(\n                padding: const EdgeInsets.all(12),\n                child: Row(\n                  children: [\n                    ClipRRect(\n                      borderRadius: BorderRadius.circular(12),\n                      child: Image.network(\n                        dish.imageUrl,\n                        width: 90,\n                        height: 90,\n                        fit: BoxFit.cover,\n                      ),\n                    ),\n                    const SizedBox(width: 16),\n                    Expanded(\n                      child: Text(\n                        dish.name,\n                        style: Theme.of(context).textTheme.titleMedium,\n                      ),\n                    ),\n                    const Icon(Icons.chevron_right),\n                  ],\n                ),\n              ),\n            ),\n          );\n        },\n      ),\n    );\n  }\n}","L'app mostra tre card cliccabili. Il tap causerà un errore finché non creiamo `DishDetailPage` nel passo successivo.",{"id":50,"position":51,"title":52,"body":53,"code_snippet":54,"code_language":39,"expected_result":55,"demo_url":41,"video_url":41},568,3,"Creare la schermata di dettaglio","La pagina di dettaglio mostra l'immagine grande in alto, il nome e la descrizione. Anche qui, per ora, nessun `Hero`: prima facciamo funzionare la navigazione.\n\nUsiamo un `CustomScrollView` non necessario per un esempio così semplice: bastano una `Column` e un `SingleChildScrollView` per evitare overflow su schermi piccoli.","\u002F\u002F lib\u002Fdish_detail_page.dart\nimport 'package:flutter\u002Fmaterial.dart';\nimport 'dish.dart';\n\nclass DishDetailPage extends StatelessWidget {\n  const DishDetailPage({super.key, required this.dish});\n\n  final Dish dish;\n\n  @override\n  Widget build(BuildContext context) {\n    return Scaffold(\n      appBar: AppBar(title: Text(dish.name)),\n      body: SingleChildScrollView(\n        child: Column(\n          crossAxisAlignment: CrossAxisAlignment.start,\n          children: [\n            Image.network(\n              dish.imageUrl,\n              width: double.infinity,\n              height: 260,\n              fit: BoxFit.cover,\n            ),\n            Padding(\n              padding: const EdgeInsets.all(20),\n              child: Column(\n                crossAxisAlignment: CrossAxisAlignment.start,\n                children: [\n                  Text(\n                    dish.name,\n                    style: Theme.of(context).textTheme.headlineSmall,\n                  ),\n                  const SizedBox(height: 12),\n                  Text(\n                    dish.description,\n                    style: Theme.of(context).textTheme.bodyLarge,\n                  ),\n                ],\n              ),\n            ),\n          ],\n        ),\n      ),\n    );\n  }\n}","Toccando una card si apre il dettaglio con la transizione standard di Material: la pagina entra da destra\u002Fdal basso, senza animazione condivisa.",{"id":57,"position":58,"title":59,"body":60,"code_snippet":61,"code_language":39,"expected_result":62,"demo_url":41,"video_url":41},569,4,"Aggiungere il widget Hero all'immagine","Ora la parte interessante. Avvolgiamo l'immagine **in entrambe le schermate** con un `Hero` che usa lo stesso `tag`. Il tag deve essere:\n\n- **univoco** all'interno della schermata visibile (due Hero con lo stesso tag nella stessa pagina generano un errore);\n- **identico** tra pagina di partenza e pagina di arrivo.\n\nUsare `dish.id` è la scelta perfetta. Non usare mai l'`index` della lista se l'ordine può cambiare.\n\nDurante la transizione Flutter estrae il widget dal suo posto, lo mette in un *overlay* e lo anima dal rettangolo di partenza a quello di arrivo.","\u002F\u002F Nella lista (main.dart), sostituisci il ClipRRect con:\nHero(\n  tag: 'dish-image-${dish.id}',\n  child: ClipRRect(\n    borderRadius: BorderRadius.circular(12),\n    child: Image.network(\n      dish.imageUrl,\n      width: 90,\n      height: 90,\n      fit: BoxFit.cover,\n    ),\n  ),\n),\n\n\u002F\u002F Nel dettaglio (dish_detail_page.dart), sostituisci Image.network con:\nHero(\n  tag: 'dish-image-${dish.id}',\n  child: Image.network(\n    dish.imageUrl,\n    width: double.infinity,\n    height: 260,\n    fit: BoxFit.cover,\n  ),\n),","Toccando una card, la miniatura si ingrandisce volando fino alla posizione dell'immagine grande del dettaglio. Premendo indietro l'animazione si inverte automaticamente.",{"id":64,"position":65,"title":66,"body":67,"code_snippet":68,"code_language":39,"expected_result":69,"demo_url":41,"video_url":41},570,5,"Animare anche il titolo e risolvere gli errori tipici","Possiamo animare più di un elemento per volta: aggiungiamo un `Hero` anche sul nome del piatto. C'è però un dettaglio: il `Text` cambia dimensione tra le due pagine e, durante il volo, il widget viene misurato con vincoli diversi — il risultato può essere un warning di overflow o un testo che \"salta\".\n\nLa soluzione standard è avvolgere il testo in un `Material` trasparente e in un `DefaultTextStyle`\u002F`FittedBox`, così il testo non eredita stili incoerenti e si adatta allo spazio disponibile.\n\n**Errori comuni da ricordare:**\n\n- *«There are multiple heroes that share the same tag»*: due `Hero` con lo stesso tag visibili contemporaneamente (tipico se usi un tag fisso in una lista). Usa sempre un id univoco.\n- Immagine che sfarfalla all'arrivo: succede quando la seconda pagina scarica di nuovo l'immagine. Usa `cached_network_image` o precarica con `precacheImage`.\n- Hero dentro un `ListView` non ancora costruito: se l'elemento di partenza non è visibile, l'animazione di ritorno non parte. È normale.","\u002F\u002F Widget riutilizzabile per il titolo animato\nclass HeroTitle extends StatelessWidget {\n  const HeroTitle({super.key, required this.tag, required this.text, required this.style});\n\n  final String tag;\n  final String text;\n  final TextStyle? style;\n\n  @override\n  Widget build(BuildContext context) {\n    return Hero(\n      tag: tag,\n      child: Material(\n        type: MaterialType.transparency,\n        child: Text(\n          text,\n          maxLines: 1,\n          overflow: TextOverflow.ellipsis,\n          style: style,\n        ),\n      ),\n    );\n  }\n}\n\n\u002F\u002F Nella lista:\nExpanded(\n  child: HeroTitle(\n    tag: 'dish-title-${dish.id}',\n    text: dish.name,\n    style: Theme.of(context).textTheme.titleMedium,\n  ),\n),\n\n\u002F\u002F Nel dettaglio:\nHeroTitle(\n  tag: 'dish-title-${dish.id}',\n  text: dish.name,\n  style: Theme.of(context).textTheme.headlineSmall,\n),","Immagine e titolo volano insieme tra le due schermate, senza warning di overflow né testo con stile sbagliato durante la transizione.",{"id":71,"position":72,"title":73,"body":74,"code_snippet":75,"code_language":39,"expected_result":76,"demo_url":41,"video_url":41},571,6,"Personalizzare la transizione: durata e forma del volo","Due parametri rendono l'effetto molto più raffinato.\n\n1. **La durata**: non si imposta sull'`Hero` ma sulla `PageRoute`, perché l'Hero segue l'animazione della rotta. Creiamo una `MaterialPageRoute` con `transitionDuration` personalizzata estendendo `PageRouteBuilder`.\n2. **`flightShuttleBuilder`**: permette di decidere cosa mostrare *durante* il volo. È utile, per esempio, per mantenere gli angoli arrotondati mentre l'immagine si ingrandisce, invece di farli sparire di colpo.\n\nProva a rallentare la transizione a 800 ms per osservare l'animazione al rallentatore: è il modo migliore per capire cosa succede.","\u002F\u002F Rotta con durata personalizzata\nRoute\u003Cvoid> detailRoute(Dish dish) {\n  return PageRouteBuilder\u003Cvoid>(\n    transitionDuration: const Duration(milliseconds: 600),\n    reverseTransitionDuration: const Duration(milliseconds: 450),\n    pageBuilder: (_, __, ___) => DishDetailPage(dish: dish),\n    transitionsBuilder: (_, animation, __, child) {\n      return FadeTransition(opacity: animation, child: child);\n    },\n  );\n}\n\n\u002F\u002F Uso nella lista:\nonTap: () => Navigator.of(context).push(detailRoute(dish)),\n\n\u002F\u002F Hero con angoli arrotondati animati durante il volo\nHero(\n  tag: 'dish-image-${dish.id}',\n  flightShuttleBuilder: (context, animation, direction, fromCtx, toCtx) {\n    final radius = Tween\u003Cdouble>(begin: 12, end: 0).animate(animation);\n    return AnimatedBuilder(\n      animation: radius,\n      builder: (context, _) => ClipRRect(\n        borderRadius: BorderRadius.circular(radius.value),\n        child: Image.network(dish.imageUrl, fit: BoxFit.cover),\n      ),\n    );\n  },\n  child: ClipRRect(\n    borderRadius: BorderRadius.circular(12),\n    child: Image.network(dish.imageUrl, width: 90, height: 90, fit: BoxFit.cover),\n  ),\n)","La transizione dura 600 ms in andata e 450 ms al ritorno, con l'immagine che perde gradualmente gli angoli arrotondati mentre si ingrandisce.",{"id":78,"position":79,"title":80,"body":81,"code_snippet":82,"code_language":39,"expected_result":83,"demo_url":41,"video_url":41},572,7,"Rifinire: precaricare le immagini ed evitare lo sfarfallio","Ultimo tocco di qualità. Con `Image.network` l'immagine grande viene scaricata quando la pagina di dettaglio si costruisce: se la connessione è lenta vedrai un rettangolo vuoto atterrare al posto della foto.\n\nDue rimedi:\n\n- `precacheImage` prima di navigare, così l'immagine è già nella cache di Flutter;\n- il pacchetto `cached_network_image`, che condivide la cache tra le due schermate.\n\nInoltre, ricorda che `Hero` funziona con qualsiasi widget: icone, `Container` colorati, avatar, persino un `FloatingActionButton` che si trasforma in una schermata. Prova a sostituire l'immagine con un `CircleAvatar` per vedere l'effetto.","onTap: () async {\n  \u002F\u002F Scarica l'immagine grande prima di aprire il dettaglio\n  await precacheImage(NetworkImage(dish.imageUrl), context);\n  if (!context.mounted) return;\n  Navigator.of(context).push(detailRoute(dish));\n},","La transizione è fluida e continua: l'immagine non lampeggia all'arrivo sulla pagina di dettaglio, nemmeno con connessione lenta.",[],1789205507578]