Mobilni razvoj
FlutterPristupačnost u FlutteruRazvoj mobilnih aplikacijaA11ySemanticsTalkBackVoiceOver

Vodič za pristupačnost u Flutteru: Semantics, redoslijed fokusa i dinamički tekst kako treba

AO
Adrijan Omićević
·14 min čitanja

# Što ćete izraditi u ovom vodiču#

Ovo je praktičan vodič za pristupačnost u Flutteru fokusiran na tri područja koja uzrokuju najviše produkcijskih problema: Semantics, redoslijed fokusa i dinamički tekst. Dobit ćete i checklistu, copy-paste isječke te ponovljivu rutinu testiranja za TalkBack i VoiceOver.

Ako već održavate design system, uskladite svoja a11y pravila s odlukama o tipografiji i bojama. Ovo se dobro nadovezuje na naš post o temiranju i tipografiji u Flutteru: Temiranje u Flutter design systemu: Material 3, dinamičke boje, tipografija.

# Zašto je pristupačnost u Flutteru važna u 2026.#

Pristupačnost nije “nišni” zahtjev. WHO procjenjuje da više od 1,3 milijarde ljudi globalno živi s nekim oblikom invaliditeta, a mobitel im je često primarni kanal. U EU se zahtjevi za pristupačnost pooštravaju kroz rokove European Accessibility Acta, a mnogi timovi danas a11y probleme tretiraju kao blokere za release.

Iz perspektive proizvoda, poboljšanja pristupačnosti obično podižu ukupnu kvalitetu UX-a: jasnije oznake, bolji tap targeti, manje “odrezanih” layouta i predvidljivija navigacija. To znači manje ticketova prema podršci i višu konverziju na formama i checkout flowovima.

🎯 Ključna poruka: Ako vaš app radi na 200 posto skaliranja teksta uz screen reader i tipkovnicu, obično radi bolje za sve.

# Osnove pristupačnosti: kako Flutter izlaže vaš UI#

Flutter gradi semantics tree uz render tree. Screen readeri i alati za switch access čitaju semantics tree, ne vaš widget tree. Vaš je zadatak osigurati da semantičke uloge, oznake, vrijednosti i akcije točno opisuju UI i odgovaraju očekivanjima korisnika.

Tri praktične posljedice:

  1. 1
    Ako UI crtate ručno bez ugrađenih kontrola, lako je napraviti nešto što izgleda kao gumb, ali je nevidljivo asistivnoj tehnologiji.
  2. 2
    Redoslijed čitanja nije uvijek isti kao vizualni redoslijed, posebno kod stackova, overlaya i animacija.
  3. 3
    Dinamički tekst može razbiti layoute ako ne dizajnirate za skaliranje od prvog dana.

# Preduvjeti#

ZahtjevVerzijaNapomene
Flutter3.22+Radi i sa starijim verzijama, ali primjeri pretpostavljaju aktualni stable
Android uređaj ili emulatorAndroid 10+Potrebno za TalkBack i Switch Access provjere
iOS uređaj ili simulatoriOS 16+Potrebno za VoiceOver provjere
Osnovno znanje widget testiranja—Korisno za Semantics asercije; vidi link o testiranju niže

Za strategiju testiranja, ovo se nadopunjuje s: Flutter testing pyramid: unit, widget, integration, golden.

# Semantics kako treba: oznake, uloge, vrijednosti i akcije#

Prvo preferirajte ugrađene kontrole#

Najlakši način da dobijete ispravne semantics postavke jest koristiti ElevatedButton, IconButton, Switch, CheckboxListTile, TextField, DropdownButton i ListTile umjesto da ih izmišljate ispočetka. Ugrađeni widgeti dolaze s ispravnim ulogama i accessibility akcijama za svaku platformu.

Kad baš morate napraviti custom kontrolu, omotajte je u Semantics i učinite je fokusabilnom i “actionable”.

Praktični Semantics obrasci (copy-paste)#

1) Gumbi samo s ikonom: uvijek dodajte tooltip ili semantičku oznaku

Dart
IconButton(
  icon: const Icon(Icons.delete),
  tooltip: 'Delete item',
  onPressed: () => deleteItem(),
)

Na mnogim platformama tooltip postaje pristupačna oznaka. Ako to preskočite, screen reader može izgovoriti samo “Button” ili besmislen naziv ikone.

2) Grupiranje sadržaja: spojite semantics za karticu u redu

Korisno je za list iteme kada želite jednu najavu umjesto čitanja svakog childa.

Dart
Semantics(
  container: true,
  label: 'Order 1832, delivered, total 48 euros',
  hint: 'Double tap to view details',
  button: true,
  child: InkWell(
    onTap: () => openOrder(),
    child: const OrderCard(),
  ),
)

Koristite container: true kada widget čini semantičku granicu.

3) Sakrijte dekorativne elemente

Ako je ikona isključivo dekorativna, izuzmite je kako ne bi zagušila najave.

Dart
Row(
  children: const [
    ExcludeSemantics(
      child: Icon(Icons.star),
    ),
    SizedBox(width: 8),
    Text('Featured'),
  ],
)

4) Live region najave za asinkrona ažuriranja

Za snackbare, greške i dinamičke status poruke, najavite promjene.

Dart
Semantics(
  liveRegion: true,
  child: Text(statusMessage),
)

Koristite štedljivo. Pretjerivanje s live regionima stvara bučno i frustrirajuće iskustvo.

💡 Savjet: Kad lokalizirate, tretirajte semantičke oznake kao stringove vidljive korisniku. Ako lokalizirate UI tekst, ali ne i semantics, screen reader postaje iskustvo na miješanom jeziku.

Semantics checklist za interaktivne widgete#

Koristite ovo kao listu za build review.

StavkaKako izgleda “dobro”Brza provjera
UlogaButton se čita kao button, switch kao switchTalkBack kaže “Button” ili “Switch” kako treba
OznakaOpisuje akciju ili sadržaj“Delete item” a ne “Trash”
HintOpcionalno, objašnjava ishod“Double tap to view details”
StanjeSelected, checked, expanded se najavljujeToggle najavi on ili off
VrijednostSlideri, progress indikatori izlažu vrijednostiVoiceOver čita postotak
Tap targetNajmanje 48 x 48 logičkih pikselaUključite pointer overlay ili izmjerite u devtools

# Redoslijed fokusa i navigacija tipkovnicom#

Problemi s fokusom su broj jedan accessibility problem tipa “osjeća se pokvareno”, posebno u flowovima poput logina, checkouta i onboardinga.

Razumijevanje redoslijeda fokusa u Flutteru#

Flutterov redoslijed fokusa određuju traversal policy i redoslijed widgeta. Vizualni trikovi u layoutu poput Stack mogu stvoriti nesklad između onoga što korisnik vidi i onoga po čemu screen reader navigira.

Po potrebi koristite FocusTraversalGroup i ordered traversal.

Primjer: eksplicitan redoslijed fokusa za formu

Dart
FocusTraversalGroup(
  policy: OrderedTraversalPolicy(),
  child: Column(
    children: const [
      FocusTraversalOrder(
        order: NumericFocusOrder(1),
        child: EmailField(),
      ),
      FocusTraversalOrder(
        order: NumericFocusOrder(2),
        child: PasswordField(),
      ),
      FocusTraversalOrder(
        order: NumericFocusOrder(3),
        child: SubmitButton(),
      ),
    ],
  ),
)

Ovo postaje kritično kad imate uvjetni UI, bannere ili inline validacijske widgete koji se pojave iznad polja.

Učinite custom widgete fokusabilnima i “actionable”#

Ako gradite custom gumb s GestureDetector, često nije fokusabilan tipkovnicom i možda ne izlaže ispravne semantics. Radije koristite InkWell ili TextButton. Ako baš morate ići custom, kombinirajte FocusableActionDetector sa semantics.

Dart
class AccessibleTileButton extends StatelessWidget {
  const AccessibleTileButton({
    super.key,
    required this.label,
    required this.onActivate,
  });
 
  final String label;
  final VoidCallback onActivate;
 
  @override
  Widget build(BuildContext context) {
    return FocusableActionDetector(
      actions: <Type, Action<Intent>>{
        ActivateIntent: CallbackAction<ActivateIntent>(
          onInvoke: (intent) => onActivate(),
        ),
      },
      child: Semantics(
        button: true,
        label: label,
        onTap: onActivate,
        child: InkWell(
          onTap: onActivate,
          child: Padding(
            padding: const EdgeInsets.all(16),
            child: Text(label),
          ),
        ),
      ),
    );
  }
}

Ovaj obrazac osigurava:

  • Screen readeri vide gumb s oznakom.
  • Korisnici tipkovnice mogu aktivirati Enterom ili Spaceom putem ActivateIntent.
  • Korisnici dodira i dalje dobivaju InkWell feedback.

⚠️ Upozorenje: Ne oslanjajte se samo na semantics onTap za podršku tipkovnici. Mnogi accessibility korisnici navigiraju hardverskim tipkovnicama, switch uređajima ili desktop platformama gdje fokus i aktivacija moraju raditi bez dodira.

Upravljanje fokusom za dijaloge, bottom sheetove i overlaye#

Čest problem: fokus ostane “iza” dijaloga ili prvi fokus završi na ikoni za zatvaranje umjesto na naslovu.

Praktična pravila:

  • Pomaknite fokus na prvi smisleni element kad se modal otvori.
  • Vratite fokus na element koji je otvorio modal kad se modal zatvori.
  • Osigurajte da screen reader ne može navigirati na sadržaj u pozadini.

U Flutteru showDialog i showModalBottomSheet uglavnom upravljaju focus trapom, ali custom overlayi to mogu pokvariti. Koristite FocusScope i postavite inicijalni fokus.

Dart
final FocusNode confirmNode = FocusNode();
 
@override
void dispose() {
  confirmNode.dispose();
  super.dispose();
}
 
void openConfirmDialog(BuildContext context) {
  showDialog(
    context: context,
    builder: (context) {
      return AlertDialog(
        title: const Text('Delete item'),
        content: const Text('This action cannot be undone.'),
        actions: [
          TextButton(
            onPressed: () => Navigator.pop(context),
            child: const Text('Cancel'),
          ),
          TextButton(
            focusNode: confirmNode,
            onPressed: () => confirmDelete(),
            child: const Text('Delete'),
          ),
        ],
      );
    },
  );
 
  Future.microtask(() => confirmNode.requestFocus());
}

# Standardi kontrasta i tap targeta koji se stvarno isporučuju#

Problemi s kontrastom su česti kad timovi usvoje dinamičke boje, gradijente ili “suptilan” UI. Za tekst, WCAG cilja omjer kontrasta 4.5:1 za normalan tekst i 3:1 za veliki tekst. Čak i ako ne radite formalnu certifikaciju, ovi pragovi su odlična baza za mobilne aplikacije.

Praktična checklist kontrasta#

PodručjeMinimalni ciljNapomene
Tekst u tijelu4.5:1Najčešći fail je “mutni” sivi tekst
Veliki tekst3:1Veliko znači oko 18pt regular ili 14pt bold
Ikone koje prenose značenje3:1Dekorativne ikone mogu biti izuzete iz zahtjeva
Indikatori fokusaJasno vidljiviPosebno za navigaciju tipkovnicom
Disabled tekstI dalje čitljivIzbjegavajte ekstremno nizak kontrast za disabled stanja

Ako koristite Material 3 dynamic color, validirajte kontrast i u light i u dark temi. Ovo je izravno vezano uz odluke design systema: Temiranje u Flutter design systemu: Material 3, dinamičke boje, tipografija.

ℹ️ Napomena: Problemi s kontrastom često se pojave samo na određenim OEM ekranima. Testirajte barem na jednom “nižem” Android uređaju gdje se svjetlina i kalibracija boja razlikuju od flagshipa.

Osigurajte minimalne tap targete#

Čak i savršeni semantics propadaju ako korisnici ne mogu pogoditi target. Koristite minimum 48 x 48 logičkih piksela za kontrole.

Dart
IconButton(
  constraints: const BoxConstraints(minWidth: 48, minHeight: 48),
  padding: EdgeInsets.zero,
  icon: const Icon(Icons.close),
  tooltip: 'Close',
  onPressed: () => Navigator.pop(context),
)

# Dinamički tekst i skaliranje teksta bez lomljenja layouta#

Skaliranje teksta je mjesto gdje se mnogi Flutter UI-ji raspadnu. Korisnici često povećaju font na 130 posto ili 200 posto. Praktičan cilj je: aplikacija ostaje upotrebljiva na 200 posto skaliranja teksta bez odrezanog teksta, bez preklapanja gumba i bez skrivenih polja u formama.

Pravila koja sprječavaju 90 posto problema#

  1. 1
    Izbjegavajte fiksne visine za containere s tekstom.
  2. 2
    Koristite Expanded i Flexible unutar rowova.
  3. 3
    Za kritične informacije preferirajte wrap umjesto ellipsisa.
  4. 4
    Testirajte s “dugim” lokalima, posebno njemačkim i finskim.
  5. 5
    Izbjegavajte forsiranje text scale na 1.0 osim za brand-kritične assete gdje pristupačnost nije pogođena.

Primjer: row koji se skalira bez rezanja

Dart
Row(
  crossAxisAlignment: CrossAxisAlignment.start,
  children: [
    const Icon(Icons.info),
    const SizedBox(width: 12),
    Expanded(
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: const [
          Text(
            'Account security',
            style: TextStyle(fontWeight: FontWeight.w600),
          ),
          SizedBox(height: 4),
          Text(
            'Enable two-factor authentication to protect your account.',
          ),
        ],
      ),
    ),
  ],
)

Ne borite se protiv text scale factora#

Ponekad timovi “stisnu” skaliranje teksta kako bi sačuvali layout. To obično mijenja konzistentnost dizajna za isključivanje korisnika.

Ako stvarno morate ograničiti skaliranje za određenu komponentu, napravite to lokalno i namjerno, i zadržite kontrole upotrebljivima.

Dart
MediaQuery(
  data: MediaQuery.of(context).copyWith(
    textScaler: const TextScaler.linear(1.1),
  ),
  child: const PriceBadge(),
)

Ovaj obrazac koristite samo kada je komponenta ornamentalna. Nemojte ga koristiti za forme, postavke, content ekrane ili onboarding copy.

Unos teksta: oznake, greške i helper tekst#

Text fieldovi su a11y “hotspot”. Najbolji obrazac je koristiti InputDecoration s label, hint i error, uz smislen error message.

Dart
TextFormField(
  decoration: const InputDecoration(
    labelText: 'Email',
    hintText: 'name@company.com',
  ),
  autovalidateMode: AutovalidateMode.onUserInteraction,
  validator: (value) {
    if (value == null || value.trim().isEmpty) return 'Email is required';
    if (!value.contains('@')) return 'Enter a valid email address';
    return null;
  },
)

Izbjegavajte generičke greške poput “Invalid input”. Korisnicima trebaju konkretne upute.

# Praktična checklist za pristupačnost u Flutteru (spremno za release)#

Koristite ovu checklistu tijekom PR reviewa i prije releasea.

Semantics#

  1. 1
    Svaki interaktivni element ima smislenu oznaku.
  2. 2
    Kontrole samo s ikonom imaju tooltip ili semantičku oznaku.
  3. 3
    Dekorativne ikone i slike su izuzete iz semantics.
  4. 4
    Dinamička ažuriranja koja su bitna koriste liveRegion štedljivo.
  5. 5
    Custom widgeti izlažu ulogu i akcije, ne samo geste.

Fokus i navigacija#

  1. 1
    Redoslijed fokusa odgovara vizualnom redoslijedu na svakom ekranu.
  2. 2
    Nema focus trapova izvan modala, a modali ispravno “trappaju” fokus.
  3. 3
    Aktivacija tipkovnicom radi za custom kontrole putem ActivateIntent.
  4. 4
    Indikatori fokusa su vidljivi na desktop i web buildovima, ako je primjenjivo.
  5. 5
    Nakon zatvaranja dijaloga, fokus se vraća na kontrolu koja ga je otvorila.

Kontrast i targeti#

  1. 1
    Tekst zadovoljava 4.5:1 ili 3:1 za veliki tekst.
  2. 2
    Ikone koje prenose značenje imaju barem 3:1.
  3. 3
    Tap targeti su najmanje 48 x 48 logičkih piksela.
  4. 4
    Disabled stanja ostaju čitljiva i nisu jedini indikator stanja.

Skaliranje teksta#

  1. 1
    Nema odrezanih naslova, gumba ili labela u formama na 200 posto skaliranja.
  2. 2
    Rowovi koriste Expanded ili wrap umjesto fiksnih širina.
  3. 3
    Kritični tekst ne ovisi o ellipsisu da bi prenio značenje.
  4. 4
    Layout ostaje upotrebljiv u oba smjera (portrait/landscape) gdje je podržano.

# Testiranje: TalkBack, VoiceOver i automatizirane provjere#

Ručni koraci testiranja za TalkBack (Android)#

  1. 1
    Uključite TalkBack u Android accessibility postavkama.
  2. 2
    Otvorite aplikaciju i prvo navigirajte bez touch explorationa, koristeći swipe desno i swipe lijevo.
  3. 3
    Provjerite:
    • Svaka kontrola se najavljuje s oznakom i ulogom.
    • Redoslijed ima smisla na svakom ekranu.
    • Stanja togglea se najavljuju kao on ili off.
  4. 4
    Testirajte touch exploration povlačenjem prsta i slušanjem najava.
  5. 5
    Prođite kroz primarni flow, npr. sign up ili checkout, bez gašenja TalkBacka.

Ručni koraci testiranja za VoiceOver (iOS)#

  1. 1
    Uključite VoiceOver u Settings.
  2. 2
    Navigirajte swipe desno i swipe lijevo, zatim rotor gdje je relevantno.
  3. 3
    Provjerite:
    • Gumbi najavljuju oznaku fokusiranu na akciju.
    • Headings ili grupiranja su smisleni na ekranima s puno sadržaja.
    • Alerti i greške se najavljuju, a ne samo vizualno prikazuju.

Automatizirano testiranje: Semantics u widget testovima#

Možete provjeriti da oznake postoje i da se nodeovi mogu tapnuti. To ne zamjenjuje testiranje na uređaju, ali brzo hvata regresije.

Dart
testWidgets('Delete button exposes accessible label', (tester) async {
  await tester.pumpWidget(const MyApp());
 
  final semantics = tester.getSemantics(find.byIcon(Icons.delete));
  expect(semantics.label, 'Delete item');
  expect(semantics.hasAction(SemanticsAction.tap), true);
});

Za kompletnu strategiju testiranja, kombinirajte ove provjere s integration testovima i golden testovima: Flutter testing pyramid: unit, widget, integration, golden.

💡 Savjet: Dodajte jedan widget test fokusiran na semantics za svaki kritični ekran. Ovi testovi su jeftini za održavanje i sprječavaju “label drift” kad se UI tekst promijeni.

# Česte zamke kod custom widgeta#

1) GestureDetector kao gumb#

GestureDetector bez semantics često stvara nevidljive kontrole. Koristite InkWell ili TextButton. Ako morate koristiti GestureDetector, dodajte Semantics(button: true, onTap: ...) i podršku za aktivaciju tipkovnicom.

2) Stackovi i overlayi kvare redoslijed čitanja#

Stack može uzrokovati da screen reader prolazi redoslijedom kreiranja widgeta, što možda ne odgovara vizualnom poretku. Popravite s:

  • eksplicitnim traversal redoslijedom u kritičnim dijelovima
  • pažljivim redoslijedom widgeta
  • grupiranjem pomoću Semantics(container: true)

3) Ponovno korištenje iste semantičke oznake za više stavki#

Liste koje stalno najavljuju “Button” ili “Item” su neupotrebljive. Uključite jedinstvene informacije poput naziva stavke, statusa i cijene.

Loš obrazac: “Open item” za svaki red.
Dobar obrazac: “Open invoice 1832, due tomorrow, 48 euros”.

4) Skaliranje teksta otkriva skrivene bugove layouta#

Hardkodirane visine, baseline alignment hackovi i pixel-perfect constraintovi često režu tekst na 130 posto i više. Popravite dopuštanjem wrapa i izbjegavanjem fiksnih visina oko teksta.

5) Oslanjanje samo na boju#

Error stanja koja samo pocrvene padaju kod daltonista i teže su uočljiva za screen readere. Dodajte ikone, helper tekst i semantičke najave.

Ako vaš tim radi i web UI-je, mentalni model se preklapa s ARIA-om i upravljanjem fokusom tipkovnicom. Ova poveznica je korisna za zajedničke standarde: React checklist za pristupačnost: ARIA i fokus tipkovnice.

# Ključne poruke#

  • Koristite ugrađene Material i Cupertino widgete kad god možete i dodajte tooltip za akcije samo s ikonom kako biste garantirali smislene oznake.
  • Kontrolirajte redoslijed fokusa s FocusTraversalGroup i ordered traversal na složenim ekranima, posebno formama i stacked layoutima.
  • Učinite custom kontrole aktivirljivima tipkovnicom s FocusableActionDetector i izložite ulogu i akcije putem Semantics.
  • Dizajnirajte za 200 posto skaliranja teksta izbjegavanjem fiksnih visina, korištenjem Expanded i dopuštanjem wrapa na kritičnom sadržaju.
  • Testirajte svaki release end-to-end s TalkBackom i VoiceOverom te dodajte Semantics asercije u widget testove kako biste spriječili regresije.

# Zaključak#

Pristupačnost u Flutteru nije jednokratni zadatak. To je skup defaulta, ponovljivih obrazaca i testnih navika koje sprječavaju regresije kako aplikacija raste.

Ako želite brz, produkcijski “a11y pass” za svoj Flutter app, Samioda vam može pomoći auditirati semantics i redoslijed fokusa, popraviti probleme sa skaliranjem teksta i kontrastom te dodati automatiziranu pokrivenost da sve ostane stabilno. Javite se i predložit ćemo konkretnu checklistu i plan implementacije za vaš codebase.

FAQ

Share
A
Adrijan OmićevićOsnivač i senior developer

Osnivač i senior developer u Samiodi. 8+ godina iskustva u izradi React, Next.js, Flutter i n8n rješenja za klijente diljem Europe.

Više iz kategorije Mobilni razvoj

Sve →

Trebate pomoć s projektom?

Gradimo prilagođena rješenja koristeći tehnologije iz ovog članka. Senior tim, fiksne cijene.