[{"data":1,"prerenderedAt":78},["ShallowReactive",2],{"tutorial-accessibilita-avanzata-in-flutter-semantics-screen-reader-e-contrasto":3,"comments-tutorial-accessibilita-avanzata-in-flutter-semantics-screen-reader-e-contrasto":77},{"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":34},99,"Accessibilità avanzata in Flutter: Semantics, screen reader e contrasto","accessibilita-avanzata-in-flutter-semantics-screen-reader-e-contrasto","Rendi la tua app Flutter davvero accessibile: albero semantico, merge ed esclusione dei nodi, live region, focus order, contrasto WCAG e test automatici con SemanticsTester.","L'accessibilità non è un extra: in molti mercati è un requisito legale (European Accessibility Act) e in tutti è una questione di qualità.\n\nFlutter costruisce, accanto all'albero dei widget e a quello di rendering, un **albero semantico** che viene esposto a TalkBack (Android), VoiceOver (iOS) e agli screen reader desktop\u002Fweb. In questa guida avanzata vediamo come controllarlo in modo chirurgico: fondere o escludere nodi, annunciare cambiamenti dinamici, gestire l'ordine di lettura, garantire target tattili e contrasto conformi alle WCAG 2.1 AA, e infine verificare tutto con i test automatici di `flutter_test`.\n\n**Prerequisiti**: conoscenza dell'albero dei widget, dei test di widget e di `MediaQuery`.","https:\u002F\u002Fflutter.it\u002Fstorage\u002Ftutorials\u002Ff358ea4f-0bfd-4cb8-acb8-d2dd713eb06d.jpg","https:\u002F\u002Fimages.unsplash.com\u002Fphoto-1733412505442-36cfa59a4240?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w5NzA2NTJ8MHwxfHJhbmRvbXx8fHx8fHx8fDE3ODk3OTIzOTZ8&ixlib=rb-4.1.0&q=80&w=1080",{"name":12,"author_url":13,"photo_url":14},"Mohammad Rahmani","https:\u002F\u002Funsplash.com\u002F@afgprogrammer","https:\u002F\u002Funsplash.com\u002Fphotos\u002Fa-computer-screen-with-a-program-running-on-it-3Sx3hSQcQIA","https:\u002F\u002Fwww.youtube.com\u002Fwatch?v=KU4aENB6QTA",{"youtube_id":17,"duration_seconds":18,"published_at":19},"KU4aENB6QTA",156,"2026-09-19T09:06:39+00:00","advanced",45,"3.x","published","2026-09-19T04:33:17+00:00","Accessibilità avanzata in Flutter con Semantics","Guida advanced all'accessibilità in Flutter: Semantics, MergeSemantics, live region, focus order, contrasto WCAG e test automatici con flutter_test.",{"id":28,"name":29,"slug":30},3,"Best practice","best-practice",{"id":32,"name":33},1,"Flutter Bot",[35,43,50,56,63,70],{"id":36,"position":32,"title":37,"body":38,"code_snippet":39,"code_language":40,"expected_result":41,"demo_url":42,"video_url":42},661,"Ispezionare l'albero semantico","Prima di scrivere codice, osserva cosa vede davvero lo screen reader.\n\n1. Attiva `debugSemantics` nei DevTools (tab *Flutter Inspector* → **Show Semantics Debugger**) oppure imposta `showSemanticsDebugger: true` sul `MaterialApp`.\n2. Da terminale usa `debugDumpSemanticsTree()` per stampare l'albero.\n\nL'albero semantico è molto più piccolo di quello dei widget: decorazioni, `Container` vuoti e `Padding` non producono nodi. I problemi tipici sono nodi **duplicati** (icona + testo letti separatamente) e nodi **senza label** (`IconButton` senza tooltip, immagini senza descrizione).","import 'package:flutter\u002Frendering.dart';\n\nvoid dumpSemantics() {\n  \u002F\u002F Chiamalo da un bottone di debug o da un hot-reload hook.\n  debugDumpSemanticsTree();\n}\n\n\u002F\u002F In alternativa, overlay visuale:\nMaterialApp(\n  showSemanticsDebugger: true, \u002F\u002F solo in debug!\n  home: const HomePage(),\n);","dart","In console compare l'albero dei SemanticsNode con label, flag e rettangoli; sullo schermo vedi i riquadri con le label lette dallo screen reader.",null,{"id":44,"position":45,"title":46,"body":47,"code_snippet":48,"code_language":40,"expected_result":49,"demo_url":42,"video_url":42},662,2,"Modellare i nodi: Semantics, Merge ed Exclude","Tre widget coprono il 90% dei casi:\n\n- **`Semantics`**: crea o annota un nodo (`label`, `hint`, `value`, `button`, `header`, `onTap`…).\n- **`MergeSemantics`**: fonde i figli in un unico nodo, così una card con icona, titolo e sottotitolo viene letta in un'unica frase.\n- **`ExcludeSemantics`**: rimuove dal sottoalbero i nodi ridondanti (es. un'icona puramente decorativa).\n\nRegola d'oro: **una zona interattiva = un nodo**. Se l'utente può toccare la card, la card deve essere un solo nodo con flag `button` e una `label` completa.\n\nPer le immagini decorative usa `ExcludeSemantics`; per quelle informative `Semantics(image: true, label: ...)`.","class ProductCard extends StatelessWidget {\n  const ProductCard({super.key, required this.name, required this.price, required this.onTap});\n\n  final String name;\n  final double price;\n  final VoidCallback onTap;\n\n  @override\n  Widget build(BuildContext context) {\n    return Semantics(\n      button: true,\n      label: '$name, prezzo ${price.toStringAsFixed(2)} euro',\n      hint: 'Apre la scheda prodotto',\n      onTap: onTap,\n      \u002F\u002F I figli non devono generare nodi propri.\n      child: ExcludeSemantics(\n        child: InkWell(\n          onTap: onTap,\n          child: Padding(\n            padding: const EdgeInsets.all(12),\n            child: Row(\n              children: [\n                const Icon(Icons.shopping_bag_outlined),\n                const SizedBox(width: 12),\n                Expanded(child: Text(name)),\n                Text('€ ${price.toStringAsFixed(2)}'),\n              ],\n            ),\n          ),\n        ),\n      ),\n    );\n  }\n}","Lo screen reader legge una sola frase per card (\"Scarpe running, prezzo 89,90 euro, pulsante\") invece di tre frammenti separati.",{"id":51,"position":28,"title":52,"body":53,"code_snippet":54,"code_language":40,"expected_result":55,"demo_url":42,"video_url":42},663,"Contenuti dinamici: live region, annunci e ordine di lettura","Quando la UI cambia senza interazione diretta (caricamento completato, errore, contatore aggiornato) lo screen reader non se ne accorge. Hai due strumenti:\n\n- `Semantics(liveRegion: true, ...)`: il nodo viene riannunciato a ogni cambio di label.\n- `SemanticsService.announce(testo, direzione)`: annuncio one-shot, utile dopo un'azione (es. \"Elemento eliminato\").\n\nPer l'ordine di lettura, quando il layout visivo non corrisponde all'ordine logico (griglie, `Stack`, barre laterali), usa `Semantics(sortKey: OrdinalSortKey(n))`: i nodi con sort key vengono letti in ordine crescente all'interno dello stesso contenitore.\n\n**Attenzione**: non abusare di `liveRegion` su liste lunghe, genererebbe un flusso continuo di annunci.","class StatusBanner extends StatelessWidget {\n  const StatusBanner({super.key, required this.message});\n  final String message;\n\n  @override\n  Widget build(BuildContext context) {\n    return Semantics(\n      liveRegion: true,\n      label: message,\n      child: ExcludeSemantics(child: Text(message)),\n    );\n  }\n}\n\nFuture\u003Cvoid> deleteItem(BuildContext context, Item item) async {\n  await repository.delete(item.id);\n  SemanticsService.announce(\n    '${item.name} eliminato',\n    Directionality.of(context),\n  );\n}\n\n\u002F\u002F Ordine di lettura forzato\nStack(\n  children: [\n    Semantics(sortKey: const OrdinalSortKey(1), child: const Header()),\n    Semantics(sortKey: const OrdinalSortKey(2), child: const Body()),\n  ],\n);","TalkBack\u002FVoiceOver annunciano automaticamente i cambi di stato e leggono i nodi nell'ordine logico definito dalle sort key.",{"id":57,"position":58,"title":59,"body":60,"code_snippet":61,"code_language":40,"expected_result":62,"demo_url":42,"video_url":42},664,4,"Contrasto, dimensioni dei target e scala del testo","Le WCAG 2.1 AA richiedono contrasto **4.5:1** per il testo normale e **3:1** per testo grande (≥18pt o 14pt bold) e per i componenti UI. Flutter espone `Color.computeLuminance()`: puoi calcolare il rapporto a runtime e far fallire un test se un token del tema non è conforme.\n\nAltri due requisiti spesso ignorati:\n- **Target tattili minimi 48x48 dp**: avvolgi i controlli piccoli con `ConstrainedBox` o usa `materialTapTargetSize: MaterialTapTargetSize.padded`.\n- **Scala del testo**: non bloccare mai `textScaler` a 1.0. Al massimo limita con `TextScaler.linear(...).clamp(...)` e verifica il layout a scala 2.0 evitando altezze fisse.","double contrastRatio(Color a, Color b) {\n  final l1 = a.computeLuminance();\n  final l2 = b.computeLuminance();\n  final hi = l1 > l2 ? l1 : l2;\n  final lo = l1 > l2 ? l2 : l1;\n  return (hi + 0.05) \u002F (lo + 0.05);\n}\n\nbool isAaCompliant(Color fg, Color bg, {bool largeText = false}) =>\n    contrastRatio(fg, bg) >= (largeText ? 3.0 : 4.5);\n\n\u002F\u002F Clamp ragionevole della scala testo (non disattivarla!)\nMaterialApp(\n  builder: (context, child) {\n    final mq = MediaQuery.of(context);\n    return MediaQuery(\n      data: mq.copyWith(\n        textScaler: mq.textScaler.clamp(minScaleFactor: 1.0, maxScaleFactor: 2.0),\n      ),\n      child: child!,\n    );\n  },\n  home: const HomePage(),\n);","I colori del tema superano la soglia 4.5:1, i controlli hanno area tattile ≥48dp e la UI resta leggibile con testo al 200%.",{"id":64,"position":65,"title":66,"body":67,"code_snippet":68,"code_language":40,"expected_result":69,"demo_url":42,"video_url":42},665,5,"Testare l'accessibilità in modo automatico","`flutter_test` include le **accessibility guideline** ufficiali: eseguile nella suite di widget test per bloccare le regressioni in CI.\n\n- `androidTapTargetGuideline` → target ≥48dp\n- `iOSTapTargetGuideline` → target ≥44dp\n- `textContrastGuideline` → contrasto WCAG AA\n- `labeledTapTargetGuideline` → ogni elemento toccabile ha una label\n\nInoltre puoi asserire la struttura semantica con `tester.getSemantics()` e il matcher `matchesSemantics`. Ricorda di chiamare `SemanticsBinding.instance.ensureSemantics()` (tramite `tester.ensureSemantics()`) e di fare `handle.dispose()` a fine test.","import 'package:flutter\u002Fmaterial.dart';\nimport 'package:flutter_test\u002Fflutter_test.dart';\n\nvoid main() {\n  testWidgets('la home rispetta le linee guida di accessibilità', (tester) async {\n    final handle = tester.ensureSemantics();\n    await tester.pumpWidget(const MyApp());\n\n    await expectLater(tester, meetsGuideline(androidTapTargetGuideline));\n    await expectLater(tester, meetsGuideline(iOSTapTargetGuideline));\n    await expectLater(tester, meetsGuideline(labeledTapTargetGuideline));\n    await expectLater(tester, meetsGuideline(textContrastGuideline));\n\n    handle.dispose();\n  });\n\n  testWidgets('la card prodotto è un unico nodo semantico', (tester) async {\n    final handle = tester.ensureSemantics();\n    await tester.pumpWidget(const MyApp());\n\n    expect(\n      tester.getSemantics(find.byType(ProductCard).first),\n      matchesSemantics(\n        label: 'Scarpe running, prezzo 89.90 euro',\n        hint: 'Apre la scheda prodotto',\n        isButton: true,\n        hasTapAction: true,\n      ),\n    );\n\n    handle.dispose();\n  });\n}","`flutter test` passa; se in futuro qualcuno riduce un bottone sotto i 48dp o usa un grigio poco contrastato, la build in CI fallisce.",{"id":71,"position":72,"title":73,"body":74,"code_snippet":75,"code_language":40,"expected_result":76,"demo_url":42,"video_url":42},666,6,"Checklist finale e verifica sul dispositivo","I test automatici coprono la conformità misurabile, non l'esperienza reale. Prima della release:\n\n1. **Android**: attiva TalkBack e naviga l'intera app solo con gesti di scorrimento.\n2. **iOS**: attiva VoiceOver e prova il rotore; verifica anche *Aumenta contrasto* e *Riduci movimento*.\n3. Disattiva le animazioni decorative quando `MediaQuery.disableAnimationsOf(context)` è `true`.\n4. Verifica che i dialog e i bottom sheet catturino il focus (`ModalRoute` lo fa di default con `barrierDismissible`) e che il focus torni al trigger alla chiusura.\n5. Controlla che nessun elemento interattivo sia raggiungibile solo tramite hover o drag.\n\nDocumenta le scelte semantiche nella tua design system: label e hint dovrebbero essere definite insieme ai componenti, non improvvisate schermata per schermata.","@override\nWidget build(BuildContext context) {\n  final reduceMotion = MediaQuery.disableAnimationsOf(context);\n  return AnimatedOpacity(\n    opacity: visible ? 1 : 0,\n    duration: reduceMotion ? Duration.zero : const Duration(milliseconds: 300),\n    child: child,\n  );\n}","L'app è navigabile end-to-end con screen reader attivo, rispetta le preferenze di sistema su movimento e contrasto ed è pronta per un audit WCAG AA.",[],1792316930351]