Действия и события
Важным свойством любого приложения является обработка действий пользователя и реакция пользовательского интерфейса (UI) на эти действия.
Duit предоставляет мощный и расширяемый событийно-ориентированный API, который позволяет описывать на сервере специальные структуры, определяющие, как фреймворк должен реагировать на взаимодействие с элементами UI и то, каким образом должен быть изменен интерфейс в результате взаимодействия. За это отвечает Action&Event API.
Действия
Действие - описанная на стороне сервера структура, определяющая что должно произойти после взаимодействия.
В Duit существует три основных типа действий. Они отличаются друг от друга тем, как они будут обработаны и какие механизмы фреймворка будут задействованы.
TransportAction
TransportAction - действия, задействующие транспортный слой Duit. Описание действия содержит название эндпоинта на стороне сервера, к которому будет идти обращение в ходе выполнения, список зависимостей действия, а также опционально дополнительные данные (HttpActionMetainfo).
{
"executionType": 0 // 0 - transport, 1 - local, 2 - script
"dependsOn": [
{"tagret": "key_in_obj", "id":"id1"}
{"tagret": "key_in_ob2j", "id":"id2"}
],
"event": "/some/endpoint",
"meta": {
"method": "POST",
},
}
LocalAction
Локальное действие мгновенно выполняет связанное событие или группу событий. Оно может быть полезно, когда надо изменить состояние UI, но обращение к серверу для этого не требуется.
{
"executionType": 1 // 0 - transport, 1 - local, 2 - script
"event": {...}, //related event
}
ScriptAction
Специальный тип действия, который содержит в своем описании динамический скрипт, который будет выполняться в интегрированной среде выполенения. Подробнее об использовании скриптов читайте в соотвествующем разделе документации.
{
"executionType": 2 // 0 - transport, 1 - local, 2 - script
"dependsOn": [
{"tagret": "key_in_obj", "id":"id1"}
{"tagret": "key_in_ob2j", "id":"id2"}
],
"script": {
"sourceCode": "fun test() {...}", //script text
"functionName": "test", //function to call
"meta": {...} //additional info
}
}
Опции выполнения действий
Duit предоставляет возможность управления частотой выполнения действий через механизм ExecutionOptions. Это позволяет контролировать, как часто действие может быть выполнено, что особенно полезно для предотвращения излишних запросов к серверу.
ExecutionOptions
Опции выполнения действий поддерживают два модификатора:
throttle- ограничивает частоту выполнения действия. Действие будет выполнено максимум один раз за указанный период времени. При повторных вызовах в течение периода выполнения будут игнорироваться.debounce- откладывает выполнение действия до окончания периода бездействия. Выполнение откладывается до тех пор, пока не пройдет указанное время без новых вызовов.
{
"executionType": 0,
"dependsOn": [...],
"event": "/some/endpoint",
"meta": {
"method": "POST"
},
"executionOptions": {
"modifier": "throttle",
"duration": 500
}
}
Пример для debounce:
{
"executionType": 0,
"event": "/search",
"executionOptions": {
"modifier": "debounce",
"duration": 300
}
}
Идемпотентное выполнение действий
Для действий TransportAction и ScriptAction Duit поддерживает механизм идемпотентного выполнения. При включённой идемпотентности повторные вызовы действия с идентичными входными данными (собранными из dependsOn) подавляются — запрос на сервер не отправляется.
Используется хеш-кэш: при каждом вызове вычисляется хеш данных из зависимостей. Если хеш совпадает с последним успешным выполнением, действие не выполняется.
Параметры:
idempotent(bool) — включает режим идемпотентного выполнения. По умолчаниюfalse.suppressionTTL(int, миллисекунды) — опционально. Время, по истечении которого разрешается повторное выполнение с теми же данными. Если не указан, подавление дубликатов действует бессрочно (пока данные не изменятся).
Используйте идемпотентность для избежания лишних запросов при быстрых повторных нажатиях кнопок или при срабатывании нескольких виджетов с одинаковым действием и данными.
{
"executionType": 0,
"dependsOn": [{"target": "query", "id": "search_input"}],
"event": "/api/search",
"idempotent": true,
"suppressionTTL": 5000
}
В примере выше действие с идентичным текстом поиска не будет выполняться повторно в течение 5 секунд после последнего выполнения.
Зависимости действий
В ходе работы с приложением пользователь может использовать такие элементы UI, как TextField, CheckBox, Radio и тд. Использование подобных элементов подразумевает сбор и использование данных, которые ввел пользовател ь.
Для работы с такими случаями TransportAction и ScriptAction обладают свойством dependsOn, которое является списоком объектов вида {"tagret": "key_in_obj", "id":"id1"} (где target - ключ свойства результирущего объекта, а id - идентификатор контроллируемого виджета), из которых требуется собрать значения и использовать при выполнении действия.
При выполнении действия, для которого указаны зависимости, Duit попытается обратиться к контроллерам этих элементов для получения их текущих значений из атрибутов. Классы атрибутов, которые могут изменять в ходе работы свое хранимое значение и имеют геттер для его получения, наследуются от класса AttendedModel<T>. Например, класс TextFieldAttributes.
В ходе сбора данных согласно списку зависимостей будет создан результирующий объект с данными. Далее он будет передан транспортному слою, который способен подготовить его к передаче. Например, в случае с HTTP GET запросом, объект будет преобразован в query params и добавлен в URL запроса, а в случае POST запроса - добавлен в тело запроса.
Используя этот механизм можно реализовывать формы, данные которых будут отправлены на сервер в ходе выполениния действия.
События
Событие - результат выполнения действия, описывает поведение UI после его успешного выполнения. Duit поддерживает несколько видов базовых действий, каждый из которых будет разобран ниже:
UpdateEvent
Событие отвечает за обновление контрллируемых элементов интерфейса. Описание события содержит объект updates,
в котором ключами являются id контрллируемых виджетов (для поиска и использования нужного контроллера), а значениями - новые атрибуты виджета.
{
"type": "update",
"updates": {
"text_1": {
"data": "New text",
// "color": "#DCDCDC" сan`t be handled
"style": {
"color": "#DCDCDC"
}
}
}
}
Парсинг новых атрибутов не поддерживает "плоские" значения. Если свойство является сложносоставным (например, TextStyle), оно должно повторять структуру оригинального класса.
CommandEvent
Событие, которое отправляет команду контроллеру для выполнения специальных операций. Команды позволяют серверу напрямую взаимодействовать с контроллерами виджетов, вызывая специфическое поведение.
Базовая структура CommandEvent:
{
"type": "command",
"controllerId": "widget_id",
"commandData": {
"type": "animation",
// ... специфичные для типа команды данные
}
}
Поддерживаемые типы команд
| Тип | Описание | API Reference |
|---|---|---|
animation | Управление анимацией свойств | AnimationCommand |
bottomSheet | Показ/скрытие модальных окон | BottomSheetCommand |
dialog | Показ/скрытие диалоговых окон | DialogCommand |
pageView | Управление PageView/PageController | PageViewCommand |
focusNode | Управление фокусом элементо в | FocusNodeCommand |
Команда анимации (animation)
{
"type": "command",
"controllerId": "animated_widget_id",
"commandData": {
"type": "animation",
"animatedPropKey": "style",
"method": 0,
"trigger": "onAction"
}
}
Параметры:
animatedPropKey- ключ анимируемого свойстваmethod- метод анимации:0(forward),1(repeat),2(reverse),3(toggle)trigger- триггер:onEnter(при появлении) илиonAction(при действии)
Команда BottomSheet
{
"type": "command",
"controllerId": "overlay",
"commandData": {
"type": "bottomSheet",
"action": "open",
"content": {...},
"isScrollControlled": true,
"isDismissible": true,
"enableDrag": true,
"showDragHandle": true,
"backgroundColor": "#FFFFFF"
}
}
Параметры:
action- действие:openилиclosecontent- содержимое (дерево виджетов)isScrollControlled,isDismissible,enableDrag- настройки поведенияonClose- действие при закрытии
Команда Dialog
{
"type": "command",
"controllerId": "overlay",
"commandData": {
"type": "dialog",
"action": "open",
"content": {...},
"barrierDismissible": true,
"useSafeArea": true
}
}
Команда PageView
{
"type": "command",
"controllerId": "page_view_id",
"commandData": {
"type": "pageView",
"action": "animateToPage",
"page": 2,
"duration": 300,
"curve": "easeInOut"
}
}
Поддерживаемые действия:
nextPage,previousPage- переход между страницамиanimateToPage,animateTo- анимированный переходjumpToPage,jumpTo- мгновенный переход
Команда FocusNode
{
"type": "command",
"controllerId": "text_field_id",
"commandData": {
"type": "focusNode",
"action": "requestFocus"
}
}
Поддерживаемые действия:
requestFocus- запросить фокусunfocus- снять фокусnextFocus,previousFocus- переход между элементамиfocusInDirection- переход в направлении (up,down,left,right)
Подробную информацию о каждом типе команды смотрите в разделе API Reference.
TimerEvent
Событие, которое будет запущено по истечении таймера. Описание содержит вложенное событие или группу событий и длительность таймера.
{
"type": "timer",
"timerDelay": 1000, // in ms
"event": {...}, //nested event
}
SequencedEventGroup
Группа событий, позволяющиая последовательно выполнять вложенные события с заданным интервалом.
{
"type": "sequenced",
"delay": 1000, // in ms
"events": [
{...}, //event 1
{...}, //event 2
{...}, //event 3
], //nested events
}
CommonEventGroup
Группа событий, позволяющиая выполнять вложенные события.
{
"type": "grouped",
"events": [
{...}, //event 1
{...}, //event 2
{...}, //event 3
], //nested events
}
Порядок выполнения действий не гарантирован!
NavigationEvent, OpenUrlEvent и CustomEvent
Эта группа событий требует регистрации внешних обработчиков через метод attachExternalHandler драйвера. Вместо использования интерфейса ExternalEventHandler, теперь используется механизм регистрации отдельных обработчиков для каждого типа события.
Для регистрации используется перечисление UserDefinedHandlerKind, которое определяет тип события:
navigation— для событий навигации (NavigationEvent).openUrl— для открытия внешних ссылок (OpenUrlEvent).custom— для пользовательских событий (CustomEvent).
Все обработчики должны соответствовать сигнатуре UserDefinedEventHandler:
typedef UserDefinedEventHandler = FutureOr<void> Function(
BuildContext context,
String path,
Object? extra,
);
Пример регистрации обработчиков:
driver.attachExternalHandler(
UserDefinedHandlerKind.navigation,
(context, path, extra) {
// Логика перехода на другой экран
},
);
driver.attachExternalHandler(
UserDefinedHandlerKind.openUrl,
(context, url, _) {
// Логика открытия URL
},
);
NavigationEvent — применяется в случаях, если необходимо выполнить переход на другой экран приложения, не являющийся Duit-экраном.
OpenUrlEvent — предназначено для открытия внешних ссылок в браузере устройства.
CustomEvent — специальный тип события, позволяющий обрабатывать события, которые не были предусмотрены разработчиками. Полезно в случаях гибридной и нтеграции Duit с приложением.
Внешние потоки событий
Duit позволяет интегрировать внешние источники событий (например, WebSockets, Firebase или события нативной платформы) в систему обработки событий драйвера. События из этих потоков будут автоматически обрабатываться драйвером и могут вызывать обновления UI или выполнение зарегистрированных обработчиков.
Для добавления потока используется метод addExternalEventStream:
final websocketStream = WebSocketChannel.connect(
Uri.parse('ws://example.com'),
).stream.map((data) => jsonDecode(data) as Map<String, dynamic>);
driver.addExternalEventStream(websocketStream);
Важные особенности:
- Драйвер автоматически подписывается на поток при его добавлении и отменяет подписку при вызове
dispose. - Вы можете добавить несколько потоков, они будут обрабатываться параллельно.
- Структура событий в потоке должна соответствовать ожидаемому формату Duit или обрабатываться зарегистрированными внешними обработчиками.
NullEvent
Сервисное событие. Парсер событий возвращает его в том случае, если объект события не может быть корректно обработан.
Система удалённых команд (RemoteCommand)
Система удалённых команд — это механизм, позволяющий серверу напрямую управлять поведением виджетов через контроллеры. В отличие от UpdateEvent, который обновляет атрибуты виджета, команды вызывают специфическое поведение: запуск анимаций, управление навигацией PageView, управление фокусом, отображение модальных окон и другие операции.
Архитектура
┌─────────────┐ ┌──────────────┐ ┌─────────────────────┐ ┌────────────┐
│ Server │ ───▶ │ CommandEvent │ ───▶ │ UIElementController │ ───▶ │ Widget │
└─────────────┘ └──────────────┘ └─────────────────────┘ └────────────┘
│
▼
┌──────────────┐
│ RemoteCommand│
│ (base) │
└──────────────┘
│
┌─────────────────┼─────────────────┐
▼ ▼ ▼
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ Animation │ │ BottomSheet │ │ PageView │
│ Command │ │ Command │ │ Command │
└──────────────┘ └──────────────┘ └──────────────┘
Базовый класс RemoteCommand
Все команды наследуются от базового класса RemoteCommand:
base class RemoteCommand {
final String controllerId; // ID контроллера-получателя
final String type; // Тип команды
final Map<String, dynamic> commandData; // Данные команды
const RemoteCommand({
required this.controllerId,
required this.type,
required this.commandData,
});
}
Жизненный цикл команды
- Создание на сервере — сервер формирует JSON-описание команды
- Парсинг —
CommandEvent.fromJson()создаёт объектRemoteCommand - Специализация —
SpecCommand(command).specify()преобразует базовую команду в типизированную (например,AnimationCommand) - Маршрутизация — драйвер находит контроллер по
controllerId - Доставка — команда отправляется в
commandChannelконтроллера - Обработка — виджет получает команду и выполняет соответствующее действие
Обработка команд в контроллере
Контроллеры виджетов подписываются на канал команд через метод listenCommand:
controller.listenCommand((command) async {
// Pattern matching для определения типа команды
switch (command) {
case AnimationCommand(:final method, :final animatedPropKey):
await _handleAnimation(method, animatedPropKey);
break;
case BottomSheetCommand(:final action, :final content):
await _handleBottomSheet(action, content);
break;
case PageViewNextPageCommand(:final duration, :final curve):
await _pageController.nextPage(duration: duration, curve: curve);
break;
case FocusNodeRequestFocusCommand(:final nodeId):
_focusNode.requestFocus();
break;
}
});
Отправка команд программно
Помимо получения команд с сервера, вы можете отправлять команды программно:
// Получение контроллера
final controller = driver.getController("widget_id");
// Создание и отправка команды
final command = AnimationCommand(
controllerId: "widget_id",
type: "animation",
commandData: {"type": "animation", "animatedPropKey": "style", "method": 0},
animatedPropKey: "style",
method: AnimationMethod.forward,
trigger: AnimationTrigger.onAction,
);
await controller.emitCommand(command);
Специальные идентификаторы контроллеров
Некоторые команды используют зарезервированные идентификаторы:
| ID | Назначение |
|---|---|
overlay | Глобальный контроллер для BottomSheet и Dialog |
Преимущества системы команд
- Декларативность — команды описываются в JSON и могут быть сгенерированы на сервере
- Типобезопасность — каждый тип команды имеет строго типизированные свойства
- Расширяемость — легко добавлять новые типы команд
- Разделение ответственности — логика выполнения команды инкапсулирована в виджете
- Асинхронность — команды обрабатываются асинхронно через Stream
Используйте команды для операций, которые не могут быть выражены через обновление атрибутов: анимации, навигация, фокус, модальные окна.
Полную документацию по каждому типу команды смотрите в разделе API Reference.