Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
Для чего нужно дерево семантики?
Дерево семантики (Semantics Tree) — это структура в Flutter, которая описывает смысловое значение и назначение UI элементов. Это критично для доступности приложений и позволяет программам (вспомогательным технологиям) понимать структуру интерфейса.
Что такое дерево семантики?
Семантика — это информация о СМЫСЛЕ элемента, а не просто его визуальном представлении.
Визуальный элемент:
┌──────────────┐
│ [ OK ] │
└──────────────┘
С семантикой:
┌──────────────┐
│ [ OK ] │ Это кнопка
└──────────────┘ Для подтверждения действия
Она отключена (disabled)
Требует фокус доступа
Три уровня семантики Flutter
1. Widget Tree (описание UI)
ElevatedButton(label: 'OK')
2. Render Tree (визуализация)
RenderButton, paint(), layout()
3. Semantics Tree (смысл) ← ВОТ ЭТО
Button {
label: 'OK',
enabled: true,
onTap: () {},
}
Зачем нужна семантика?
1. Доступность для слабовидящих
// ❌ Без семантики
Icon(
Icons.favorite,
color: Colors.red,
)
// Screen reader: "Image" (только картинка)
// ✅ С семантикой
Icon(
Icons.favorite,
color: Colors.red,
semanticLabel: 'Add to favorites', // Для экранного диктора
)
// Screen reader: "Add to favorites, button"
2. Экранные дикторы (Screen Readers)
class AccessibleButton extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Semantics(
button: true, // Это кнопка
enabled: true, // Доступна
label: 'Save document', // Что делает
onTap: () => saveDocument(),
child: FloatingActionButton(
onPressed: () => saveDocument(),
child: Icon(Icons.save),
),
);
}
}
// Экранный диктор скажет:
// "Save document, button, double-tap to activate"
3. Роботизированное тестирование
// Test может найти элемент по семантике
testWidgets('find button by semantic label', (WidgetTester tester) async {
await tester.pumpWidget(MyApp());
// Поиск по семантическому ярлыку
expect(
find.bySemanticsLabel('Save document'),
findsOneWidget,
);
});
Основные семантические свойства
1. Button (кнопка)
Semantics(
button: true,
label: 'Delete item',
onTap: () {},
child: GestureDetector(
onTap: () {},
child: Icon(Icons.delete),
),
)
2. Slider (ползунок)
Slider(
value: 50,
onChanged: (value) {},
semanticFormatterCallback: (value) {
return 'Volume ${value.round()}%';
},
)
3. Checkbox (флажок)
Checkbox(
value: isChecked,
onChanged: (value) {},
semanticLabel: 'Enable notifications',
)
4. Image (изображение)
Image.asset(
'assets/photo.jpg',
semanticLabel: 'Profile photo of John Doe',
)
Пример: Доступное приложение
class AccessibleForm extends StatefulWidget {
@override
State<AccessibleForm> createState() => _AccessibleFormState();
}
class _AccessibleFormState extends State<AccessibleForm> {
String _name = '';
bool _agreeToTerms = false;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Registration Form'),
),
body: Padding(
padding: EdgeInsets.all(16),
child: Column(
children: [
// Текстовое поле с семантикой
Semantics(
label: 'Name field',
textField: true,
enabled: true,
child: TextField(
decoration: InputDecoration(
labelText: 'Name',
hintText: 'Enter your name',
),
onChanged: (value) {
setState(() => _name = value);
},
),
),
SizedBox(height: 16),
// Checkbox с семантикой
Semantics(
checkbox: true,
checked: _agreeToTerms,
label: 'I agree to the terms and conditions',
onTap: () {
setState(() => _agreeToTerms = !_agreeToTerms);
},
child: Row(
children: [
Checkbox(
value: _agreeToTerms,
onChanged: (value) {
setState(() => _agreeToTerms = value ?? false);
},
),
Text('I agree to the terms'),
],
),
),
SizedBox(height: 16),
// Кнопка с семантикой
Semantics(
button: true,
enabled: _name.isNotEmpty && _agreeToTerms,
label: 'Register',
onTap: _agreeToTerms && _name.isNotEmpty
? () => _register()
: null,
child: ElevatedButton(
onPressed: _agreeToTerms && _name.isNotEmpty
? () => _register()
: null,
child: Text('Register'),
),
),
],
),
),
);
}
void _register() {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Registered: $_name')),
);
}
}
Проверка семантики
1. Включить отладку семантики
import 'package:flutter/rendering.dart';
void main() {
debugPrintSemantics = true; // Вывести семантику
runApp(MyApp());
}
2. Вывести дерево семантики
import 'package:flutter/rendering.dart';
// В коде приложения
debugDumpSemantics();
// Или через инспектор
// Flutter DevTools → Accessibility
Семантические роли
class SemanticRolesExample extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Column(
children: [
// Button role
Semantics(
button: true,
label: 'Send',
child: ElevatedButton(onPressed: () {}, child: Text('Send')),
),
// Slider role
Semantics(
slider: true,
label: 'Brightness',
child: Slider(value: 50, onChanged: (_) {}),
),
// Checkbox role
Semantics(
checkbox: true,
checked: true,
label: 'Enable notifications',
child: Checkbox(value: true, onChanged: (_) {}),
),
// Radio button role
Semantics(
radioButton: true,
label: 'Option A',
selected: true,
child: Radio(value: 'A', groupValue: 'A', onChanged: (_) {}),
),
// Text field role
Semantics(
textField: true,
label: 'Search',
child: TextField(decoration: InputDecoration(labelText: 'Search')),
),
],
);
}
}
Лучшие практики доступности
1. Всегда используй semanticLabel для иконок
// ❌ Плохо
IconButton(
icon: Icon(Icons.delete),
onPressed: () {},
)
// ✅ Хорошо
IconButton(
icon: Icon(Icons.delete),
tooltip: 'Delete',
onPressed: () {},
)
// Или явно
Semantics(
button: true,
label: 'Delete item',
onTap: () {},
child: Icon(Icons.delete),
)
2. Используй Semantics для кастомных виджетов
class CustomButton extends StatelessWidget {
final String label;
final VoidCallback onPressed;
CustomButton({required this.label, required this.onPressed});
@override
Widget build(BuildContext context) {
return Semantics(
button: true,
label: label,
onTap: onPressed,
child: GestureDetector(
onTap: onPressed,
child: Container(
padding: EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.blue,
borderRadius: BorderRadius.circular(8),
),
child: Text(label),
),
),
);
}
}
3. Обозначай отключенные состояния
Semantics(
button: true,
enabled: isEnabled, // ← Важно!
label: 'Submit',
onTap: isEnabled ? () => submit() : null,
child: ElevatedButton(
onPressed: isEnabled ? () => submit() : null,
child: Text('Submit'),
),
)
4. Группируй связанные элементы
Semantics(
container: true,
label: 'Contact information',
child: Column(
children: [
Text('Email: user@example.com'),
Text('Phone: +1-234-567-8900'),
],
),
)
Тестирование доступности
testWidgets('accessible button test', (WidgetTester tester) async {
await tester.pumpWidget(MyApp());
// Проверить семантику
expect(
find.bySemanticsLabel('Delete item'),
findsOneWidget,
);
// Нажать на кнопку по семантике
await tester.tap(find.bySemanticsLabel('Delete item'));
await tester.pumpAndSettle();
});
Инструменты для проверки доступности
1. Flutter DevTools
flutter pub global activate devtools
flutter pub global run devtools
2. Accessibility Inspector (встроенный)
flutter run
# Затем Ctrl+U в терминале для доступности
3. Screen readers (для тестирования)
- Android: TalkBack (Settings → Accessibility → TalkBack)
- iOS: VoiceOver (Settings → Accessibility → VoiceOver)
Сравнение: Widget vs Semantic Tree
Widget Tree:
MyApp
└─ Scaffold
└─ Column
└─ GestureDetector
└─ Container
└─ Icon
Semantic Tree:
MyApp
└─ Container
└─ Button {
label: 'Delete',
onTap: ...
}
Вывод
Дерево семантики — это невидимый слой доступности:
✅ Позволяет экранным дикторам понимать структуру ✅ Облегчает тестирование через find.bySemanticsLabel() ✅ Обязателен для accessibility (доступность для всех) ✅ Не влияет на производительность (небольшой overhead) ✅ Улучшает UX для людей с ограничениями
Правило большого пальца: Если ты пишешь кастомный виджет и используешь GestureDetector — обверни его в Semantics!
Доступное приложение — это приложение, которое могут использовать ВСЕ, включая людей с нарушениями зрения, слуха или моторики.