Flutter mobil uygulama geliştirmede erişilebilirlik, yalnızca label eklemek değildir. Semantics ağacını doğru kurmayı, özel widget'larda eylem sağlamayı, golden ve widget testleriyle regresyonları yakalamayı ele alıyoruz.
Flutter Mobil Uygulama Geliştirmede Erişilebilirlik ve Semantics Testi
Flutter mobil uygulama geliştirmede Semantics ağacını tasarlamak
Bir ekran okuyucu, widget ağacını değil render nesnelerinin ürettiği Semantics düğümlerini okur. Bu nedenle bir Row içinde ikon, metin ve tıklanabilir alan ayrı düğümler üretiyorsa TalkBack veya VoiceOver kullanıcısı aynı kartı üç kez odaklayabilir. Flutter Inspector içindeki Toggle Semantics Debugger seçeneği ile odak sınırlarını görün; ardından kullanıcı açısından tek bir eylem olan kartı MergeSemantics ve Semantics ile birleştirin.
class OrderCard extends StatelessWidget {
const OrderCard({super.key, required this.order, required this.onOpen});
final Order order;
final VoidCallback onOpen;
@override
Widget build(BuildContext context) {
return MergeSemantics(
child: Semantics(
button: true,
label: 'Siparis ${order.number}',
value: '${order.total} TL, ${order.statusLabel}',
hint: 'Siparis detaylarini ac',
onTap: onOpen,
child: InkWell(
onTap: onOpen,
child: Row(
children: [
ExcludeSemantics(child: Icon(order.statusIcon)),
Text(order.number),
const Spacer(),
Text('${order.total} TL'),
],
),
),
),
);
}
}ExcludeSemantics burada görsel ikonu gizler, fakat onu saran Semantics düğümünün label ve eylemini silmez. Sık yapılan hata, tüm InkWell'ı ExcludeSemantics içine alıp üstüne yalnızca label eklemektir; bu durumda button: true ve onTap bildirimi yoksa ekran okuyucu öğenin aktive edilebilir olduğunu söylemez. Flutter eğitimi içinde bu ayrım özellikle önemlidir: görünür metin ile erişilebilir rol, değer, ipucu ve eylem birbirinden bağımsız alanlardır.
Flutter state management ile erişilebilir durum değişimlerini duyurmak
Bir yükleme isteği tamamlandığında ekrandaki metni değiştirmek, ekran okuyucunun bu değişikliği otomatik olarak anons edeceği anlamına gelmez. SemanticsService.announce çağrısı, odak başka bir kontroldeyken başarı veya hata gibi geçici sonuçları platform erişilebilirlik kanalına iletir. Aşağıdaki Riverpod örneğinde duyuruyu state reducer içine değil, state geçişini gözleyen UI katmanına koymak kritik ayrımdır: reducer'ın aynı state'i yeniden üretmesi veya testte tekrar çalışması halinde yinelenen sesli bildirim oluşmaz.
final saveOrderProvider = AsyncNotifierProvider<SaveOrder, void>(SaveOrder.new);
class SaveOrder extends AsyncNotifier<void> {
@override
FutureOr<void> build() {}
Future<void> save(OrderDraft draft) async {
state = const AsyncLoading();
state = await AsyncValue.guard(() => ref.read(orderApiProvider).save(draft));
}
}
class SaveOrderButton extends ConsumerWidget {
const SaveOrderButton({super.key, required this.draft});
final OrderDraft draft;
@override
Widget build(BuildContext context, WidgetRef ref) {
ref.listen<AsyncValue<void>>(saveOrderProvider, (previous, next) {
if (next.hasValue && previous?.isLoading == true) {
SemanticsService.announce('Siparis kaydedildi', Directionality.of(context));
}
if (next.hasError) {
SemanticsService.announce('Siparis kaydedilemedi', Directionality.of(context));
}
});
final saving = ref.watch(saveOrderProvider).isLoading;
return Semantics(
button: true,
enabled: !saving,
label: saving ? 'Siparis kaydediliyor' : 'Siparisi kaydet',
child: FilledButton(
onPressed: saving ? null : () => ref.read(saveOrderProvider.notifier).save(draft),
child: const Text('Kaydet'),
),
);
}
}Bu desen, flutter state management seçiminden bağımsızdır; Bloc'ta BlocListener, Provider'da addListener ile aynı geçiş filtresi uygulanabilir. Özellikle cross platform mobil uygulama geliştirme yapan ekipler, Android'de TalkBack'in ve iOS'ta VoiceOver'ın duyuru sıralamasını fiziksel cihazda doğrulamalıdır. Aynı frame içinde hem route push hem announce yapılırsa yeni route'un odak duyurusu mesajı bastırabilir; route geçişi tamamlandıktan sonra WidgetsBinding.instance.addPostFrameCallback ile duyuru yapmak daha deterministiktir.
Semantics widget testleriyle erişilebilirlik regresyonunu yakalamak
Erişilebilirlik kontrolünü manuel cihaz testine bırakmak, bir refactor sonrası kaybolan onTap veya yanlış label sorununu geç fark ettirir. Flutter'ın SemanticsTester sınıfı, widget testinde framework'ün ürettiği düğümleri eşleştirir. Testi flutter test test/order_card_test.dart ile CI aşamasına ekleyin; bu test görünür metni değil, platforma gönderilecek anlam bilgisini doğrular.
testWidgets('siparis karti tek bir buton semantigi uretir', (tester) async {
final semantics = SemanticsTester(tester);
addTearDown(semantics.dispose);
var opened = false;
await tester.pumpWidget(MaterialApp(
home: OrderCard(
order: Order(number: 'A-1042', total: 340, statusLabel: 'Hazirlaniyor'),
onOpen: () => opened = true,
),
));
expect(
semantics,
includesNodeWith(
label: 'Siparis A-1042',
value: '340 TL, Hazirlaniyor',
hint: 'Siparis detaylarini ac',
actions: <SemanticsAction>[SemanticsAction.tap],
flags: <SemanticsFlag>[SemanticsFlag.isButton],
),
);
await tester.tap(find.byType(InkWell));
expect(opened, isTrue);
});Golden test, piksel farkını bulur ama label değişimini bulmaz; bu nedenle ikisini ayrı sinyaller olarak tutun. Daha geniş ekranlar için debugDumpSemanticsTree(DebugSemanticsDumpOrder.traversalOrder) çıktısını yalnızca lokal hata ayıklamada kullanın. Bu çıktıyı doğrudan snapshot testine bağlamak kırılgandır: framework içi düğüm sırası, görünümü değiştirmeyen bir layout düzenlemesiyle farklılaşabilir. Bunun yerine iş açısından kritik düğümlerde includesNodeWith beklentileri yazın.
Özel RenderObject içinde rol, değer ve özel eylem tanımlamak
Canvas ile çizilen grafik, takvim veya sürüklenebilir kontrol, içindeki metin widget'ları olmadığı için varsayılan olarak erişilebilir değildir. RenderBox.describeSemanticsConfiguration ile rolü ve değeri tanımlayın; birden fazla sanal çocuk gerekiyorsa assembleSemanticsNode ile ayrı SemanticsNode'lar üretin. Tek düğümlü bir slider için aşağıdaki yapı yeterlidir ve platform, onIncrease ile onDecrease eylemlerini yerel erişilebilirlik hareketlerine bağlar.
class RenderZoomControl extends RenderBox {
RenderZoomControl(this.zoom, this.onZoomChanged);
double zoom;
final ValueChanged<double> onZoomChanged;
@override
void describeSemanticsConfiguration(SemanticsConfiguration config) {
super.describeSemanticsConfiguration(config);
config
..isSemanticBoundary = true
..label = 'Harita yakinlastirma'
..value = '${(zoom * 100).round()} yuzde'
..increasedValue = '${((zoom + 0.1) * 100).round()} yuzde'
..decreasedValue = '${((zoom - 0.1) * 100).round()} yuzde'
..onIncrease = () => onZoomChanged((zoom + 0.1).clamp(0.5, 3.0))
..onDecrease = () => onZoomChanged((zoom - 0.1).clamp(0.5, 3.0));
}
@override
bool get isRepaintBoundary => true;
}Buradaki ince nokta, zoom değiştiğinde yalnızca markNeedsPaint() çağırmamaktır. Semantics value da değiştiği için setter içinde markNeedsSemanticsUpdate() çağrılmalıdır; aksi halde ekran görüntüsü yeni yüzdeyi gösterirken TalkBack eski değeri okuyabilir. Dart programlama eğitimi bağlamında bu, mutable render state'in iki ayrı invalidation kanalı olduğunu gösteren pratik bir örnektir: paint ve semantics.
Semantics maliyetini profile ederek büyük listeleri sınırlandırmak
Binlerce etkileşimli satır içeren ekranlarda semantics ağacı, frame pipeline içindeki semantics aşamasına ek iş yükü getirir. Ölçümü debug modda yapmayın; assertion'lar ve debug instrumentation sonucu bozar. flutter run --profile ile uygulamayı açın, Flutter DevTools Performance görünümünde aynı 500 satırlık listeyi sabit hızla kaydırın ve önce-sonra kaydında frame'lerin 90. yüzdelik semantics süresini karşılaştırın. Aynı cihaz, aynı erişilebilirlik servisi durumu ve aynı scroll senaryosu olmadan iki trace'i kıyaslamak anlamlı değildir.
Örneğin dekoratif ayraçlar, arka plan badge'leri ve tekrar eden durum ikonları ayrı Semantics düğümü üretiyorsa, bunları hariç tutarak düğüm sayısını azaltın. Değişiklikten önce Flutter Inspector Semantics Debugger ile ekranda 40 satır için düğüm sayısını kaydedin; sonra aynı viewport'ta tekrar sayın. Hedef, bilgi kaybetmeden yalnızca görsel amaçlı düğümleri kaldırmaktır.
ListTile(
leading: ExcludeSemantics(
child: StatusDot(color: order.isLate ? Colors.red : Colors.green),
),
title: Text(order.customerName),
subtitle: Text(order.statusLabel),
trailing: ExcludeSemantics(
child: const Icon(Icons.chevron_right),
),
onTap: () => openOrder(order.id),
)Bu optimizasyonun güvenli olmasının mekanizması şudur: ListTile, title, subtitle ve onTap bilgisini kendi birleşik semantics düğümünde zaten taşır; dekoratif nokta ve chevron'un ayrı odak hedefi olması ek bilgi sağlamaz. Ancak durum yalnızca renkle ifade ediliyorsa ExcludeSemantics uygulamayın. Önce subtitle: Text(order.statusLabel) gibi metinsel eşdeğer ekleyin. Bir flutter kursu projesinde bu kontrolü acceptance kriterine dönüştürmek, erişilebilirlik maliyetini ölçülebilir hale getirir: kritik liste için düğüm sayısı, 90. yüzdelik frame süresi ve SemanticsTester kapsamı PR açıklamasında raporlanabilir.
İlgili Eğitim
YTÜSEM İlgili Eğitim
Sık Sorulan Sorular
Flutter eğitimi sırasında Semantics ile accessibilityLabel arasındaki fark nedir?
Flutter'da temel araç Semantics widget'ıdır; label, value, hint, rol flag'leri ve onTap gibi eylemleri birlikte tanımlar. Sadece label eklemek, bir kontrolün button veya slider olduğunu bildirmez. Flutter Inspector Semantics Debugger ile oluşan düğümü, ardından SemanticsTester ile SemanticsAction.tap ve SemanticsFlag.isButton beklentilerini doğrulayın.
Flutter state management değişiminde ekran okuyucuya hata nasıl duyurulur?
Riverpod ref.listen, Bloc BlocListener veya Provider listener içinde önceki ve sonraki state'i karşılaştırın. Yalnızca loading durumundan success veya error durumuna geçişte SemanticsService.announce('Kaydetme basarisiz', Directionality.of(context)) çağırın. Duyuruyu reducer veya notifier içine koymak, retry ve yeniden build senaryolarında aynı mesajın tekrar okunmasına yol açabilir.
Cross platform mobil uygulama geliştirme için Flutter erişilebilirlik testi CI'da nasıl çalıştırılır?
Kritik akışlar için widget testlerinde SemanticsTester kullanın ve CI'da flutter test çalıştırın. Android ve iOS'un gerçek ekran okuyucu davranışı farklı olduğundan, release adayı için Firebase Test Lab, BrowserStack App Automate veya fiziksel cihaz havuzunda TalkBack ve VoiceOver ile ayrı smoke test senaryosu çalıştırın. Widget testi semantics sözleşmesini, cihaz testi platform odak ve duyuru sırasını yakalar.
Flutter mobil uygulama geliştirmede Semantics performansı nasıl ölçülür?
Uygulamayı flutter run --profile ile başlatın, DevTools Performance'ta aynı scroll senaryosunun trace'ini alın ve semantics aşamasının frame başına süresini karşılaştırın. Değişiklik öncesi ve sonrası için aynı fiziksel cihazda 90. yüzdelik frame süresini kaydedin. Dekoratif ikonları ExcludeSemantics ile kaldırmadan önce, eşdeğer durum bilgisinin metin veya birleşik label içinde mevcut olduğundan emin olun.
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.



