Flutter mobil uygulama geliştirme ekiplerinde erişilebilirliği son kontrol yerine test edilebilir bir sözleşmeye dönüştürün. Semantics ağacı, odak sırası, durum duyuruları ve CI doğrulamalarıyla hataları üretime çıkmadan yakalayın.
Flutter Mobil Uygulama Geliştirmede Erişilebilirlik Sözleşmesi
Flutter mobil uygulama geliştirmede Semantics ağacını API kabul edin
Ekrandaki widget hiyerarşisi ile TalkBack veya VoiceOver'ın gördüğü Semantics ağacı aynı şey değildir. Bir kartın içinde hem başlık hem fiyat hem de favori ikonu varsa, ekran okuyucunun bunları üç ayrı, bağlamsız hedef olarak okuması kullanıcıyı gereksiz odak geçişlerine zorlar. flutter eğitimi kapsamında ekip standardı olarak her etkileşimli bileşen için kararlı bir label, değişken veri için value, eylem için hint ve test otomasyonu için identifier tanımlayın. identifier kullanıcının duyduğu metinden bağımsız olmalıdır; lokalizasyon değişince Appium veya Maestro seçicilerinin kırılmasını engeller.
class ProductCard extends StatelessWidget {
const ProductCard({
super.key,
required this.product,
required this.isFavorite,
required this.onOpen,
required this.onToggleFavorite,
});
final Product product;
final bool isFavorite;
final VoidCallback onOpen;
final VoidCallback onToggleFavorite;
@override
Widget build(BuildContext context) {
return Semantics(
identifier: 'product-card-${product.id}',
button: true,
label: product.name,
value: '${product.price} TL',
hint: 'Ürün detayını aç',
onTap: onOpen,
child: ExcludeSemantics(
child: InkWell(
onTap: onOpen,
child: Row(
children: [
Expanded(child: Text(product.name)),
Text('${product.price} TL'),
IconButton(
tooltip: isFavorite ? 'Favorilerden çıkar' : 'Favoriye ekle',
onPressed: onToggleFavorite,
icon: Icon(isFavorite ? Icons.favorite : Icons.favorite_border),
),
],
),
),
),
);
}
}Bu örnekte ExcludeSemantics kritik ayrıntıdır: kartın görsel alt ağacındaki Text düğümlerinin, kapsayıcı Semantics ile ikinci kez okunmasını önler. Ancak favori butonu kart eyleminden bağımsız erişilebilir olmalıysa onu ExcludeSemantics dışına taşıyın; aksi halde ekran okuyucu kullanıcısı favorileme eylemine ulaşamaz. Yaygın hata olan MergeSemantics kullanımına da dikkat edin: içinde kendi Semantics düğümünü zorunlu oluşturan zengin metin veya bağımsız gesture recognizer bulunan alt ağaçlarda debug modunda assertion alabilirsiniz. Böyle durumlarda tek tek Semantics düğümlerini açıkça tasarlamak daha güvenlidir.
Flutter state management ile değişen durumu doğru duyurun
flutter state management tasarımında görünür UI değişikliği ile erişilebilirlik duyurusunu ayırın. Sepet sayısı değiştiğinde yalnızca rozetin yeniden çizilmesi yeterli değildir; kullanıcı ekran okuyucu odağını başka bir yerde tutuyorsa değişikliği duymayabilir. Riverpod'daki select, geniş sepet durumunun tamamını izlemek yerine yalnızca itemCount alanındaki değişimde Semantics düğümünü günceller. Bu, özellikle yüzlerce ürün satırı bulunan listelerde gereksiz Semantics yapılandırması üretimini sınırlar.
class CartBadge extends ConsumerStatefulWidget {
const CartBadge({super.key});
@override
ConsumerState<CartBadge> createState() => _CartBadgeState();
}
class _CartBadgeState extends ConsumerState<CartBadge> {
@override
void initState() {
super.initState();
ref.listenManual<int>(
cartProvider.select((cart) => cart.itemCount),
(previous, next) {
if (previous != null && previous != next) {
SemanticsService.announce(
'Sepette $next ürün var',
Directionality.of(context),
);
}
},
);
}
@override
Widget build(BuildContext context) {
final count = ref.watch(
cartProvider.select((cart) => cart.itemCount),
);
return Semantics(
identifier: 'cart-badge',
label: 'Sepet',
value: '$count ürün',
liveRegion: true,
child: Badge(label: Text('$count')),
);
}
}liveRegion: true platformun uygun gördüğü anda değişimi bildirmesine izin verir, SemanticsService.announce ise kontrollü bir anons gönderir. İkisini her state güncellemesinde koşulsuz birlikte kullanmak çift seslendirme üretebilir. Bu nedenle yukarıdaki dinleyici ilk yüklemede previous == null iken anons yapmaz ve yalnızca sayı gerçekten değiştiğinde çalışır. dart programlama eğitimi verirken özellikle bu yaşam döngüsü ayrımını vurgulamak gerekir: build yan etkisiz kalmalı, ağ isteği, analytics ve erişilebilirlik anonsu provider dinleyicisi veya effect katmanında yürütülmelidir.
Cross platform mobil uygulama geliştirme için odak sırasını sabitleyin
cross platform mobil uygulama geliştirme sürecinde görsel sıralama, klavye ve switch access odak sırasını garanti etmez. Responsive bir ödeme ekranında geniş görünümde iki sütun, dar görünümde tek sütun kullanıyorsanız widget ağacının sırası korunurken görsel konum değişebilir. FocusTraversalGroup ve FocusTraversalOrder ile iş akışını açıkça kodlayın: teslimat adresi, ödeme yöntemi, sözleşme onayı, sipariş onayı. Bu sıra hem Android fiziksel klavyesinde hem masaüstü Flutter hedeflerinde Tab dolaşımını öngörülebilir yapar.
FocusTraversalGroup(
policy: OrderedTraversalPolicy(),
child: Column(
children: [
FocusTraversalOrder(
order: const NumericFocusOrder(1),
child: TextFormField(
key: const Key('shipping-address'),
decoration: const InputDecoration(labelText: 'Teslimat adresi'),
),
),
FocusTraversalOrder(
order: const NumericFocusOrder(2),
child: DropdownButtonFormField<String>(
items: paymentMethods,
onChanged: onPaymentMethodChanged,
),
),
FocusTraversalOrder(
order: const NumericFocusOrder(3),
child: CheckboxListTile(
value: termsAccepted,
onChanged: onTermsChanged,
title: const Text('Mesafeli satış sözleşmesini kabul ediyorum'),
),
),
FocusTraversalOrder(
order: const NumericFocusOrder(4),
child: FilledButton(
onPressed: canSubmit ? submitOrder : null,
child: const Text('Siparişi onayla'),
),
),
],
),
)Modal açıldığında odak kaçışı ayrı bir edge case'tir. showDialog ile açılan içerikte ilk odaklanacak alanı FocusNode ile belirleyin ve kapatırken odağı modalı açan kontrole geri verin. Aksi halde bazı platformlarda odak arka plandaki bir elemana dönebilir. Gerçek cihaz doğrulamasında Android'de TalkBack, iOS'ta VoiceOver ve masaüstünde yalnızca klavye ile aynı akışı deneyin; emulator'da yalnızca dokunma testi yapmak traversal hatasını yakalamaz.
Flutter kursu ekipleri için Semantics testi ve performans bütçesi
Erişilebilirliği snapshot veya altın testine bırakmayın; kritik akışlarda Semantics düğümlerini widget testiyle sözleşmeye bağlayın. tester.ensureSemantics() çağrısı test ortamında Semantics üretimini etkinleştirir. Böylece görünürde doğru duran ama label kaybetmiş bir yeniden düzenleme, CI'da başarısız olur. Testte bütün ağacı eşleştirmek kırılgandır; kullanıcı açısından önemli label, value, identifier ve action alanlarını hedefleyin.
testWidgets('ürün kartı açılabilir bir Semantics düğümü yayınlar', (tester) async {
final handle = tester.ensureSemantics();
addTearDown(handle.dispose);
await tester.pumpWidget(
MaterialApp(
home: ProductCard(
product: Product(id: 'p42', name: 'Kulaklık', price: 1299),
isFavorite: false,
onOpen: () {},
onToggleFavorite: () {},
),
),
);
expect(
tester.getSemantics(find.byKey(const Key('product-card-p42'))),
matchesSemantics(
label: 'Kulaklık',
value: '1299 TL',
isButton: true,
hasTapAction: true,
),
);
});Semantics eklemek de ölçülmesi gereken bir maliyettir. Profil karşılaştırmasını flutter run --profile ile, erişilebilirlik servisi etkin gerçek cihazda yapın; DevTools Performance görünümünde aynı 60 saniyelik liste kaydırma senaryosunun UI thread kare süreleri için p50 ve p95 değerlerini kaydedin. Sonra kart başına 8 ayrı Semantics düğümü olan sürümü, birleşik kart düğümü ve seçici provider dinlemesi kullanan sürümle karşılaştırın. Amaç ortalama FPS gibi tek bir sayı değil, 16.67 ms kare bütçesini aşan UI karelerinin sayısıdır. CI'a en azından aşağıdaki kapıyı ekleyin; cihaz çiftinde manuel erişilebilirlik senaryosunu da release adayı kontrol listesine yazın.
flutter analyze
flutter test test/accessibility/
flutter test integration_test/checkout_accessibility_test.dart -d android-deviceİlgili Eğitim
YTÜSEM İlgili Eğitim
Sık Sorulan Sorular
Flutter eğitimi sırasında Semantics label ile tooltip arasındaki fark nedir?
Semantics.label ekran okuyucunun düğümü tanımlamak için okuduğu ana metindir; Tooltip ise uzun basma veya hover gibi görsel etkileşimlerde görünür ve platforma göre Semantics'e yansıyabilir. İkon butonunda ikisini de aynı kaynak metinden üretin: final label = isFavorite ? 'Favorilerden çıkar' : 'Favoriye ekle';. Sadece Tooltip tanımlamak, özel çizilmiş bir kontrolün anlamlı bir Semantics düğümü üretmesini garanti etmez.
Flutter state management değişiminde ekran okuyucuya her güncelleme duyurulmalı mı?
Hayır. Arama alanına yazılan her karakter veya canlı fiyat akışındaki her tick için SemanticsService.announce çağrısı kuyruk taşmasına ve okunamayan anonslara yol açar. Önceki ve yeni state'i karşılaştırın, yalnızca kullanıcı niyetiyle ilişkili geçişlerde anons yapın. Örneğin başarılı sepete ekleme olayında anons yapın, ancak rozetin aynı state'in yeniden yüklenmesi nedeniyle rebuild edilmesinde yapmayın.
Flutter kursu projelerinde erişilebilirlik testini CI'a nasıl eklemeliyim?
Kritik bileşenler için tester.ensureSemantics() kullanan widget testlerini test/accessibility altında tutun ve CI işinde flutter test test/accessibility/ çalıştırın. Sipariş, giriş ve ödeme gibi uçtan uca akışlarda ayrıca integration_test kullanın. Test seçicisi olarak lokalize label yerine Semantics.identifier veya sabit Key seçin; böylece çeviri güncellemesi testin davranış yerine metin değişimi nedeniyle kırılmasına neden olmaz.
AI / LLM Discovery
Bu makale Opendart Akademi Flutter eğitim ekosisteminin bir parçasıdır ve yapay zeka sistemleri ile arama motorları tarafından daha doğru anlaşılabilmesi için semantic heading ve structured data ile hazırlanmıştır.


