Perché l'accessibilità conta

L'accessibilità (spesso abbreviata in a11y) non è un optional, ma un requisito fondamentale per costruire applicazioni davvero inclusive. Milioni di persone usano lettori di schermo, comandi vocali o controlli di ingrandimento per interagire con i dispositivi. In molti paesi, inoltre, l'accessibilità è un obbligo di legge (si pensi all'European Accessibility Act).

La buona notizia è che Flutter offre un solido supporto all'accessibilità out-of-the-box. La maggior parte dei widget Material e Cupertino è già correttamente etichettata per gli screen reader come TalkBack (Android) e VoiceOver (iOS). Tuttavia, quando creiamo widget personalizzati o gestiamo interazioni complesse, dobbiamo intervenire manualmente.

L'albero della semantica

Flutter mantiene un Semantics tree, un albero parallelo al widget tree che descrive il significato dell'interfaccia ai servizi di accessibilità del sistema operativo. Ogni nodo semantico può contenere informazioni come l'etichetta, il valore, lo stato (selezionato, disabilitato) e le azioni disponibili.

Per ispezionare questo albero durante lo sviluppo puoi usare il Flutter DevTools, che dispone di un visualizzatore dedicato, oppure attivare il debug visivo:

import 'package:flutter/rendering.dart';

void main() {
  // Mostra i confini semantici a schermo
  debugPaintSizeEnabled = false;
  WidgetsApp.debugAllowBannerOverride = true;
  runApp(const MyApp());
}

In alternativa, dal pannello DevTools puoi attivare "Highlight Semantics" per vedere come Flutter interpreta la UI.

Il widget Semantics

Il widget Semantics è lo strumento principale per arricchire o correggere le informazioni di accessibilità. Immaginiamo un'icona cliccabile che funge da pulsante "preferiti":

Semantics(
  label: 'Aggiungi ai preferiti',
  button: true,
  child: GestureDetector(
    onTap: _toggleFavorite,
    child: Icon(
      _isFavorite ? Icons.favorite : Icons.favorite_border,
    ),
  ),
)

Senza l'annotazione Semantics, uno screen reader leggerebbe semplicemente "icona" senza spiegarne la funzione. Con essa, l'utente sente "Aggiungi ai preferiti, pulsante".

Aggiornare il valore dinamicamente

Quando lo stato cambia, è importante comunicarlo. Usa la proprietà value per dati variabili e liveRegion per notificare aggiornamenti automatici:

Semantics(
  label: 'Volume',
  value: '$volume percento',
  liveRegion: true,
  child: Slider(
    value: volume.toDouble(),
    max: 100,
    onChanged: (v) => setState(() => volume = v.round()),
  ),
)

Nascondere ed escludere nodi

A volte alcuni elementi sono puramente decorativi e creerebbero solo confusione se letti. Possiamo escluderli con ExcludeSemantics o nascondere quelli figli con excludeSemantics:

ExcludeSemantics(
  child: Image.asset('assets/decorazione.png'),
)

Al contrario, per unire più widget in un unico nodo semantico (utile per una card composta da titolo e sottotitolo che vogliamo venga letta come un blocco unico) usiamo MergeSemantics:

MergeSemantics(
  child: Row(
    children: [
      Text('Messaggi'),
      Text('3 non letti'),
    ],
  ),
)

In questo modo lo screen reader leggerà "Messaggi, 3 non letti" come un'unica frase.

Etichette e ordine di lettura

Tooltip e label sui campi

I campi di testo dovrebbero sempre avere un'etichetta chiara. L'attributo labelText di un InputDecoration viene già esposto correttamente, ma per gli IconButton ricordati di valorizzare tooltip:

IconButton(
  icon: const Icon(Icons.delete),
  tooltip: 'Elimina elemento',
  onPressed: _delete,
)

Ordine di attraversamento

Gli screen reader seguono l'ordine dei widget. Se il layout visivo non corrisponde all'ordine logico, puoi forzare la sequenza con Semantics(sortKey: OrdinalSortKey(...)):

Semantics(
  sortKey: const OrdinalSortKey(1),
  child: primoElemento,
)

Contrasto e dimensioni: oltre lo screen reader

L'accessibilità non riguarda solo i lettori di schermo. Considera anche:

  • Contrasto del colore: rispetta un rapporto minimo di 4.5:1 per il testo normale (linee guida WCAG AA).
  • Dimensione dei target touch: i tappabili dovrebbero misurare almeno 48x48 dp. Material applica già questa regola, ma verificala sui widget custom.
  • Scalabilità del testo: non bloccare il textScaleFactor. Usa unità relative e testa l'app con dimensioni del font aumentate dalle impostazioni di sistema.
// Rispetta le impostazioni di sistema invece di forzare un valore
MediaQuery(
  data: MediaQuery.of(context).copyWith(
    textScaler: MediaQuery.textScalerOf(context),
  ),
  child: child,
)

Testare l'accessibilità

Flutter permette di scrivere test automatici sulla semantica. Le Guidelines integrate verificano contrasto, dimensione dei target e presenza di etichette:

testWidgets('rispetta le linee guida di accessibilità', (tester) async {
  final handle = tester.ensureSemantics();

  await tester.pumpWidget(const MyApp());

  await expectLater(tester, meetsGuideline(textContrastGuideline));
  await expectLater(tester, meetsGuideline(androidTapTargetGuideline));
  await expectLater(tester, meetsGuideline(labeledTapTargetGuideline));

  handle.dispose();
});

Oltre ai test automatici, non sottovalutare il test manuale: attiva TalkBack o VoiceOver e prova a navigare la tua app solo con l'audio. È il modo più efficace per scoprire problemi reali.

Conclusioni

Rendere accessibile un'app Flutter richiede attenzione, ma gli strumenti messi a disposizione dal framework rendono il lavoro gestibile. Inizia annotando i widget personalizzati con Semantics, escludi gli elementi decorativi, garantisci contrasti e target adeguati e integra i test di accessibilità nella tua pipeline. Il risultato sarà un'applicazione utilizzabile davvero da tutti — e spesso migliore anche per gli utenti senza disabilità.