Создание настраиваемого чекбокса с доступом с клавиатуры в Flutter
FlutterPulseЭта статья переведена специально для канала FlutterPulse. В этом канале вы найдёте много интересных вещей, связанных с Flutter. Не забывайте подписываться! 🚀

В современном разработке приложений доступность не является просто приятной функцией — это необходимо. Сегодня мы исследуем, как создать настраиваемый компонент checkbox в Flutter, который обеспечивает отличную доступность с клавиатуры, сохраняя при этом чистый и поддерживаемый код. Этот компонент позволит пользователям взаимодействовать с checkbox с помощью навигации с клавиатуры, делая ваше приложение более инклюзивным для пользователей, которые полагаются на ввод с клавиатуры или вспомогательные технологии.
Встроенный компонент Checkbox в Flutter хорошо справляется с взаимодействием мыши/сенсорным экраном, но иногда нам нужно более полный контроль над взаимодействием с клавиатуры, управлением фокусом и настраиваемыми поведениями. Наш компонент FocusedCheckBox решает эти проблемы следующим образом:
Проблема
- Предоставление последовательной навигации с клавиатуры (поддержка клавиш Enter и Space)
- Управление состояниями фокуса правильно
- Разрешение гибкого управления фокусом
- Поддержание чистой разделения проблем
Полное решение
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
class FocusedCheckBox extends StatefulWidget {
final bool isChecked;
final Function(bool) onChanged;
final FocusNode? focusNode;
const FocusedCheckBox({
super.key,
required this.isChecked,
required this.onChanged,
this.focusNode,
});
@override
State<FocusedCheckBox> createState() => _FocusedCheckBoxState();
}
class _FocusedCheckBoxState extends State<FocusedCheckBox> {
late FocusNode _focusNode;
bool _isInternalFocusNode = false;
@override
void initState() {
super.initState();
_initializeFocusNode();
}
@override
void didUpdateWidget(FocusedCheckBox oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.focusNode!= oldWidget.focusNode) {
_disposeFocusNode();
_initializeFocusNode();
}
}
void _initializeFocusNode() {
if (widget.focusNode!= null) {
_focusNode = widget.focusNode!;
_isInternalFocusNode = false;
} else {
_focusNode = FocusNode();
_isInternalFocusNode = true;
}
_focusNode.onKeyEvent = _handleKeyEvent;
}
void _disposeFocusNode() {
_focusNode.onKeyEvent = null;
if (_isInternalFocusNode) {
_focusNode.dispose();
}
}
KeyEventResult _handleKeyEvent(FocusNode node, KeyEvent event) {
if (event is KeyDownEvent) {
if (event.logicalKey == LogicalKeyboardKey.enter ||
event.logicalKey == LogicalKeyboardKey.space) {
widget.onChanged(!widget.isChecked);
return KeyEventResult.handled;
}
}
return KeyEventResult.ignored;
}
@override
void dispose() {
_disposeFocusNode();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Checkbox(
focusNode: _focusNode,
value: widget.isChecked,
onChanged: (isChecked) {
widget.onChanged(isChecked?? false);
},
);
}
}
Разбор реализации
1. Объявление компонента и свойства
class FocusedCheckBox extends StatefulWidget {
final bool isChecked;
final Function(bool) onChanged;
final FocusNode? focusNode;
Наш компонент принимает три параметра:
isChecked: текущее состояние checkbox (управляется родителем)onChanged: функция обратного вызова при изменении состояния checkboxfocusNode: необязательный внешний фокус-нод для продвинутого управления фокусом
Этот дизайн следует за конвенцией Flutter по разделению управления состоянием между родительскими и дочерними компонентами, что делает его многоразовым и предсказуемым.
2. Переменные состояния
late FocusNode _focusNode; bool _isInternalFocusNode = false;
Мы поддерживаем две важные переменные состояния:
_focusNode: фактический фокус-нод, используемый checkbox_isInternalFocusNode: флаг, отслеживающий, создали ли мы фокус-нод внутри
Это отслеживание имеет решающее значение для правильного управления памятью — мы утилизируем только те фокус-ноды, которые мы создали сами.
3. Логика инициализации
@override
void initState() {
super.initState();
_initializeFocusNode();
}
Метод initState() выполняется один раз при создании виджета. Мы сразу же настраиваем наш фокальный узел, чтобы виджет был готов для взаимодействия с самого начала.
4. Обработка обновлений виджета
@override
void didUpdateWidget(FocusedCheckBox oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.focusNode!= oldWidget.focusNode) {
_disposeFocusNode();
_initializeFocusNode();
}
}
Метод didUpdateWidget() вызывается всякий раз, когда родительский виджет перестраивается с новыми свойствами. Мы проверяем, изменился ли фокальный узел, и переинициализируем его, если необходимо. Это обеспечивает правильное адаптирование нашего виджета к динамическим изменениям фокального узла.
5. Инициализация фокального узла
void _initializeFocusNode() {
if (widget.focusNode!= null) {
_focusNode = widget.focusNode!;
_isInternalFocusNode = false;
} else {
_focusNode = FocusNode();
_isInternalFocusNode = true;
}
_focusNode.onKeyEvent = _handleKeyEvent;
}
Этот метод обрабатывает двойную природу управления фокальным узлом:
- Если предоставлен внешний фокальный узел, мы используем его (полезно для сложных сценариев управления фокусом)
- Если фокальный узел не предоставлен, мы создаем свой собственный
- Мы всегда подключаем наш обработчик событй клавиатуры, независимо от источника фокального узла
6. Обработка событй клавиатуры
KeyEventResult _handleKeyEvent(FocusNode node, KeyEvent event) {
if (event is KeyDownEvent) {
if (event.logicalKey == LogicalKeyboardKey.enter ||
event.logicalKey == LogicalKeyboardKey.space) {
widget.onChanged(!widget.isChecked);
return KeyEventResult.handled;
}
}
return KeyEventResult.ignored;
}
Вот где происходит магия для доступности клавиатуры:
- Мы реагируем только на событйя нажатия клавиш (не на событйя отпускания или повторения клавиш)
- Мы поддерживаем как клавиши Enter, так и Space, следуя стандартным соглашениям доступности
- Мы переключаем состояние флажка, вызывая функцию обратного вызова
onChangedродительского виджета - Мы возвращаем
KeyEventResult.handledчтобы предотвратить всплытие событй - Для необработанных клавиш мы возвращаем
KeyEventResult.ignoredчтобы разрешить обычную обработку
7. Очистка ресурсов
void _disposeFocusNode() {
_focusNode.onKeyEvent = null;
if (_isInternalFocusNode) {
_focusNode.dispose();
}
}
@override
void dispose() {
_disposeFocusNode();
super.dispose();
}
Правильная очистка имеет решающее значение для предотвращения утечек памяти:
- Мы удаляем обработчик событй клавиатуры, чтобы разорвать потенциальные круговые ссылки
- Мы очищаем только те фокальные узлы, которые мы создали внутренне
- Метод
dispose()обеспечивает очистку, когда виджет удаляется из дерева
8. Построение интерфейса
@override
Widget build(BuildContext context) {
return Checkbox(
focusNode: _focusNode,
value: widget.isChecked,
onChanged: (isChecked) {
widget.onChanged(isChecked?? false);
},
);
}
Метод построения интерфейса прост — мы делегируем встроенному виджету Checkbox Flutter, предоставляя наш управляемый фокальный узел и гарантируя, что функция обратного вызова всегда получает значение типа boolean.
Примеры использования
Базовое использование
bool _isChecked = false;
FocusedCheckBox(
isChecked: _isChecked,
onChanged: (value) {
setState(() {
_isChecked = value;
});
},
)
Расширенное использование с настраиваемым управлением фокусом
class MyForm extends StatefulWidget {
@override
_MyFormState createState() => _MyFormState();
}
class _MyFormState extends State<MyForm> {
final FocusNode _checkboxFocus = FocusNode();
bool _isChecked = false;
@override
Widget build(BuildContext context) {
return Column(
children: [
TextField(
onSubmitted: (_) => _checkboxFocus.requestFocus(),
),
FocusedCheckBox(
focusNode: _checkboxFocus,
isChecked: _isChecked,
onChanged: (value) {
setState(() {
_isChecked = value;
});
},
),
],
);
}
@override
void dispose() {
_checkboxFocus.dispose();
super.dispose();
}
}
Демо

Ключевые преимущества
Доступность в первую очередь
Этот виджет обеспечивает отличную навигацию с помощью клавиатуры, что делает ваше приложение доступным для пользователей, которые используют клавиатуру, программы для чтения с экрана или другие вспомогательные технологии.
Гибкое управление фокусом
Принимая необязательный внешний узел фокуса, виджет может участвовать в сложных сценариях управления фокусом, при этом работая идеально в простых случаях.
Безопасность памяти
Правильное управление ресурсами гарантирует отсутствие утечек памяти даже в динамических сценариях, где узлы фокуса часто меняются.
Поддерживаемый код
Ясная разделение проблем и хорошо документированные методы делают этот виджет легко понимаемым, изменяемым и расширяемым.
Демонстрация лучших практик
- Управляемые компоненты: Виджет не поддерживает自己的 состояние — родитель контролирует его
- Управление ресурсами: Правильная утилизация созданных ресурсов предотвращает утечки памяти
- Доступность: Поддержка стандартных взаимодействий с клавиатурой (Enter и Space)
- Гибкость: Необязательные параметры позволяют использовать как простые, так и сложные случаи
- Защитная программа: Безопасные операции и правильное обработка событий
Заключение
Создание доступных, хорошо спроектированных пользовательских виджетов в Flutter требует внимания к управлению фокусом, взаимодействиям с клавиатурой и очистке ресурсов. Этот FocusedCheckBox виджет демонстрирует, как обращаться с этими проблемами, сохраняя чистый, многоразовый код.
Следуя показанным здесь шаблонам, вы можете создать другие интерактивные пользовательские виджеты, которые обеспечивают отличный пользовательский опыт для всех пользователей, независимо от того, как они взаимодействуют с вашим приложением. Помните, доступность — это не только соблюдение требований — это создание инклюзивных опытов, которые работают хорошо для всех.