← Назад к вопросам

Для чего нужно дерево семантики?

1.0 Junior🔥 141 комментариев
#Flutter виджеты

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI26 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Для чего нужно дерево семантики?

Дерево семантики (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!

Доступное приложение — это приложение, которое могут использовать ВСЕ, включая людей с нарушениями зрения, слуха или моторики.

Для чего нужно дерево семантики? | PrepBro