Состояние
Состояние и правила
Заголовок раздела «Состояние и правила»Solo<S> хранит одно неизменяемое состояние типа S. Job объявляет,
в каких состояниях ей позволено начаться и в каких продолжаться, а
движок проверяет эти правила за неё:
Job<void> record() => run<Ready, void>( key: 'record', canStart: (state) => state.free > 0, keepWhile: (state) => !state.paused, (ctx) => ctx.each( camera.frames, (child, frame) => child.join(() => store(frame)), ).value, );Первый аргумент типа у run<W, T> задаёт рабочий тип состояния Job,
W extends S. Тело читает ctx.state как W, поэтому run<Ready, void>
может начаться только в Ready. Когда чужое обновление состояния
устанавливает paused в true, keepWhile отменяет запись; колбэку
не нужно повторять это условие. Здесь Ready, camera и store
принадлежат приложению, а ctx.each обрабатывает стрим — его жизненный
цикл разобран в Дочерние задачи и стримы.
Используйте отдельные классы, когда состояния разрешают разные операции, и общие базовые типы, когда операция может проходить через несколько состояний.
| Правило | Когда проверяется |
|---|---|
Рабочий тип W |
Перед стартом, при чужих обновлениях состояния во время тела и в контрольных точках состояния. |
canStart |
Один раз, непосредственно перед стартом Job. |
keepWhile |
Перед стартом, при чужих обновлениях состояния во время тела и в контрольных точках состояния. |
Если стартовая проверка не проходит, Job заканчивается с Cancelled
и started: false. Нарушение W или keepWhile при выполнении отменяет
Job с RulesCancelReason. Эти правила отклоняют неподходящую работу;
они не оставляют её в очереди до наступления подходящего состояния.
Чтение и изменение состояния
Заголовок раздела «Чтение и изменение состояния»(ctx) async { // Чтения — это контрольные точки: проверяются отмена и правила. final free = ctx.state.free; final ready = ctx.stateAs<Ready>(); ctx.check();
// Единственная запись, и она синхронная. ctx.emit(Recording(free: free, zoom: ready.zoom));}ctx.state, ctx.stateAs<T>() и ctx.check() проверяют отмену и правила
состояния. stateAs<T>() дополнительно требует тип T; несовпадение
отменяет Job. Методы ожидания также используют контрольные точки
состояния.
ctx.emit(next) позволяет Job опубликовать состояние за пределами своего
рабочего типа: например, инициализация может закончиться публикацией
Ready. Следующая контрольная точка отклонит такое состояние, если оно
не соответствует W, поэтому такой переход должен быть последним шагом
тела, которому нужен доступ к состоянию. canStart после emit
не повторяется.
После обновления проверяются другие работающие тела. Собственный emit
Job исключён из этой проверки правил, но всё равно проверяет отмену
до и после записи. Поэтому синхронный хук или слушатель, вызвавший ещё
одно изменение состояния, может отменить излучающую Job до возврата
из emit.
Правила перестают отменять Job после завершения её тела. Ручная отмена, закрытие контроллера и отмена родителя по-прежнему могут достичь её при ожидании детей или освобождении ресурсов. Job с обработчиками состояния также продолжают проверять, разрешено ли этим обработчикам менять состояние; см. Состояние после ошибки или отмены.
Наблюдение за состоянием
Заголовок раздела «Наблюдение за состоянием»// Чтение в любой момент.print(camera.state);
// Каждое обновление, по порядку, через микротаску. Начальное состояние// не повторяется, а публикация равного состояния тоже даёт событие.final subscription = camera.stream.listen(print);Любой код с доступом к контроллеру может читать state. К моменту
доставки события стрима state может содержать более новое значение.
| Тип | Что предоставляет |
|---|---|
SoloBase<S> |
Состояние, Job, очередь и правила. |
Solo<S> |
Всё из SoloBase и broadcast-стрим stream. |
SoloListenable<S> |
Всё из Solo и интерфейс Flutter ValueListenable<S>. |
Слушатели SoloListenable вызываются синхронно, в порядке подписки.
Его стрим остаётся асинхронным.
Внешнее состояние
Заголовок раздела «Внешнее состояние»Независимый источник — устройство, сокет — меняется, не дожидаясь
контроллера. externalSetState(next) отражает изменение, которое там
уже произошло:
final class Camera extends Solo<CameraState> { final Device device; late final StreamSubscription<bool> _link;
Camera(this.device) : super(const Ready()) { _link = device.connection.listen((connected) { // Устройство уже отключилось: отражаем факт немедленно, а не // ставим Job, которая ждала бы за текущей. if (!connected) { externalSetState(const Disconnected()); } }); }
@override Future<void> close({SoloCloseMode mode = SoloCloseMode.cancel}) async { // Сначала остановить внешний слушатель: он ещё может менять состояние. await _link.cancel(); await super.close(mode: mode); }}Метод помечен @protected и вызывается внутри наследника контроллера,
обычно слушателем, зарегистрированным там же.
Чего стоила бы другая дорога. Если бы слушатель ставил в очередь
отдельную Job для публикации Disconnected, это обновление ждало бы
за текущей Job. До тех пор контроллер сообщает о подключённом
состоянии, правила текущей Job не могут отреагировать на отключение,
а сама она может ждать ответа, который уже не придёт. Вызов
externalSetState обновляет состояние немедленно и переоценивает
работающие Job, поэтому Job, правилам которой нужно подключение,
отменяется. Её метод ожидания определяет, когда возобновится тело
и должна ли завершиться сама операция; очередь по-прежнему ждёт тела
и уборки Job перед запуском следующей корневой.
Различие в том, что означает уведомление. Если оно говорит, что независимая сущность уже изменилась, отражайте этот факт немедленно. Если оно просит контроллер выполнить работу — обновить данные, сохранить пришедшее значение, — ставьте обычную Job. Доставка события стримом сама по себе не оправдывает обход очереди.
Для собственных успеха, ошибки и отмены контроллера используйте тела Job
и их обработчики состояния. externalSetState — исключение для внешних
фактов, а не универсальный сеттер для этих операций. Он продолжает менять
state и вызывать хуки изменения после close(), тогда как закрытый
стрим Solo обновления уже не доставляет, — поэтому слушатель
останавливают первым.
Состояние после ошибки или отмены
Заголовок раздела «Состояние после ошибки или отмены»run<ProfileState, String>( // Оба только вычисляют состояние. Они выполняются после тела, детей // и уборки, и исход к этому моменту уже определён. onError: (state, error, stackTrace) => Failure(error), onCancel: (state, cancelled) => const Initial(), (ctx) async { ctx.emit(const Loading()); return ctx.wait(api.fetchName); },);Параметры onError и onCancel у run и job позволяют контроллеру
покинуть временное состояние вроде Loading, когда операция завершилась
ошибкой или была отменена. Они получают текущее состояние типа S, плюс
ошибку со стеком вызовов или исход Cancelled, и синхронно возвращают
следующее состояние.
Подходящий обработчик выполняется перед следующей Job из очереди
и перед хуком onFinish. Изменение состояния не превращает Failed
или Cancelled в Done и не помечает исход обработанным для сообщений
об ошибках.
Сохранение несовместимого внешнего состояния
Заголовок раздела «Сохранение несовместимого внешнего состояния»Итоговый обработчик не должен затирать состояние, при котором его Job
недопустима. Например, расширим состояния профиля из быстрого старта
классом Disconnected в той же библиотеке и заменим метод load
контроллера на такой вариант:
final class Disconnected extends ProfileState { const Disconnected();}
Job<String> load() => run<ProfileState, String>( key: 'load', policy: Policy.droppable, keepWhile: (state) => state is! Disconnected, onError: (state, error, stackTrace) => Failure(error), onCancel: (state, cancelled) => const Initial(), (ctx) async { ctx.emit(const Loading()); final name = await ctx.wait(api.fetchName); ctx.emit(Loaded(name)); return name; }, );Слушатель устройства вызывает externalSetState(const Disconnected())
внутри контроллера. keepWhile отвергает это состояние, отменяя загрузку
и отключая оба итоговых обработчика. Поэтому Disconnected остаётся
видимым, а не заменяется на Initial или Failure.
Если загрузку отменили вручную, пока состояние ещё совместимо, onCancel
может вернуть Initial. Обработчикам не нужны дополнительные проверки
state is Loading: правила Job уже выражают, какие состояния допускают
операцию и её итоговую поправку.
Допуск обработчиков и их ошибки
Заголовок раздела «Допуск обработчиков и их ошибки»| Что происходит | Что становится с обработчиками |
|---|---|
| Несовместимое внешнее обновление, когда бы оно ни пришло | Отключены навсегда; последующее совместимое обновление их не возвращает. |
| Родитель потерял допуск | Обработчики его детей тоже заблокированы. |
| Успех, отброшенный дубликат, тело, которое не запускалось | Не выполняются вовсе. |
Собственный emit Job |
Ничего; свои обработчики он не отключает. |
| Обработчик бросил ошибку | Ошибка сообщается; исход и очередь не затронуты. |
| Правило бросило ошибку при проверке допуска | Обработчики отключены, ошибка сообщена. Освобождение ресурсов всё равно выполняется. |
Эти дополнительные проверки относятся к Job с собственными обработчиками
состояния. Родитель без обработчиков перестаёт проверять свои правила
после завершения тела; дети остаются под своими правилами и под уже
потерянным допуском родителя. canStart проверяется только при входе.
Держите эти функции в границах вычисления состояния. Освобождение ресурсов — дело API уборки.