async_job
Future в Dart нельзя отменить. async_job добавляет к асинхронному коду
на Dart отмену, дочерние задачи и освобождение ресурсов: отменённая задача
останавливается на ближайшей контрольной точке, выполняет уборку и завершается
после своих дочерних задач, как принято в структурной конкурентности. Пакет
работает на чистом Dart, из зависимостей только meta.
Job<T> выполняет функцию, которая называется телом задачи, и запоминает, чем
она завершилась. Через await job.done можно получить исход задачи, а через
await job.value значение, возвращённое телом. Сама Job не реализует
Future: задачу можно запустить без await, это обычное дело, а не забытое
ожидание, и линты о неожидаемых future её не трогают. По той же причине
await job компилируется и ничего не ждёт; базовый набор package:lints
отмечает это правилом await_only_futures.
«Зачем задача» показывает, что задача
добавляет к Future, флагу и CancelableOperation, и даёт развёрнутый пример.
«Справочник» перечисляет каждый член
контекста и то, что он делает у отменённой задачи.
Установка
Заголовок раздела «Установка»dart pub add async_jobПлагин анализатора async_job_lints
сообщает об ошибках, которые компилируются, работают и часто проходят первый
тест, — например, об ожидании в уборке, которое так и не случается, или
о ресурсе, потерянном при отмене. Названный в analysis_options.yaml, он
включает свои предупреждения; его линты включаются по имени, как показывает его
раздел
«Установка»:
plugins: async_job_lints: ^0.1.0Быстрый старт
Заголовок раздела «Быстрый старт»Задача открывает базу и читает из неё, а кто-то отменяет её, пока идёт чтение.
Тело получает контекст ctx, и каждый вызов, которого оно ждёт через
ctx.join, служит контрольной точкой: когда задача отменена, следующая точка
бросает Cancelled. dispose закрывает базу, чем бы задача ни кончилась:
import 'package:async_job/async_job.dart';
Future<void> main() async { final job = Job<void>((ctx) async { // Проверка перед вызовом и ещё раз перед возвратом значения, // а база закрывается, чем бы Job ни кончилась. final db = await ctx.join(Database.open, dispose: (db) => db.close()); use(await ctx.join(db.readAll)); }); final outcome = job.done;
// Кто-то ушёл с экрана, пока шло чтение. await Future<void>.delayed(const Duration(milliseconds: 50));
// Ждёт тело, его детей и его уборку. await job.cancel(); print(switch (await outcome) { Done() => 'read', Failed(:final error) => 'failed: $error', Cancelled(:final reason) => 'cancelled: $reason', });}example/quick_start.dart запускает этот блок с Database, которая печатает,
когда открывается и когда закрывается. Он печатает:
database openeddatabase closedcancelled: manualctx.join дождался чтения, которое уже началось, и прочитанные данные до тела
не дошли; cancel() вернулся, когда задача кончилась вместе с уборкой. done
взят рядом с конструктором, поэтому провал, случившийся до отмены, попадает
в switch, а не роняет программу. Ошибка, которую чтение бросит уже после
отмены, исходом не становится: задача кончается Cancelled, а ошибка остаётся
неперехваченной, как показывает раздел
«Куда уходят ошибки».
Задача, отменённая сразу, тело не запускает, поэтому блок ждёт, пока чтение
не пойдёт.
Уйти от вызова
Заголовок раздела «Уйти от вызова»Future<void> main() async { final job = Job<String>((ctx) => ctx.abandonable(fetchQuote)); final outcome = job.done;
// Кто-то ушёл с экрана, пока цитата была в пути. await Future<void>.delayed(const Duration(milliseconds: 50));
// Возвращается раньше, чем ответит fetchQuote. await job.cancel(); print(await outcome);}example/walk_away.dart запускает этот блок с fetchQuote, который печатает,
когда запрос отправлен и когда приходит цитата. Он печатает:
request sentCancelled(manual)quote answeredctx.abandonable перестаёт ждать вызов при отмене, а вызов идёт дальше, и его
результат никто не заберёт: cancel() вернулся раньше, чем пришла цитата,
тогда как ctx.join в быстром старте дождался чтения. Это подходит вызову,
который не держит ничего, что задача убирает: чтение из быстрого старта под
ctx.abandonable кончилось бы на закрытой базе. Запросу, который умеет
останавливаться, отмену передают через ctx.onCancel, а ждут его через
ctx.join; раздел
«Токен через onCancel»
показывает, как это сделать и как Job.isCancellationError не пускает ошибку
каждой остановки в отчёты приложения о сбоях. Ошибка, которую вызов бросит
после отмены, остаётся неперехваченной, как и в быстром старте.
Страницы
Заголовок раздела «Страницы»| Страница | О чём |
|---|---|
| Зачем задача | Что задача добавляет к Future, флагу и CancelableOperation, и развёрнутый пример |
| Исходы | Done, Failed, Cancelled и наблюдение за провалом |
| Отмена | Контрольные точки, onCancel, uncancellable |
| С riverpod и bloc | Задача под провайдером, нотифаером или обработчиком bloc и HTTP-запрос, который она прерывает |
| Время | ctx.pause, срок через timeout |
| Дети | Job.deferred, ctx.child, ctx.run, ctx.start для ребёнка без await, глубина дерева |
| Группы | .wait, ctx.runAll, что группа отдаёт обратно |
| Цепочки | then, продолжение ребёнка, шаги подряд |
| Стримы | ctx.each, Job.each и что делают await for и listen в теле |
| Уборка | dispose, discard, ctx.disown для значения, которое задача не отдаёт |
| Правила уборки | Ресурс, переданный другой задаче, onDispose и снятие, порядок и поздние результаты |
| Наблюдение | JobObserver, имя задачи, логи |
| Ошибки | Куда идёт ошибка, JobAnswerer, Job.isCancellationError, unattended, JobObserver.all |
| Правила ошибок | Путь ошибки каждого вида, Job.debug, Job.visitErrors, редкие случаи |
| Тестирование | Задача под fakeAsync и что двигает её время |
| Своё поверх ядра | JobBase, JobContextBase, своя задача, Job.deferred, finish |
| Свой движок | Очередь, которая отказывает в cancelWith, правило в check() |
| Справочник | Каждый член контекста и что он делает у отменённой задачи; четыре слова модели |
| Изменение | Выходит в |
|---|---|
| Исправление, которое не трогает публичный API | патч-выпуске, например 1.0.1 |
| Новый класс, член или необязательный параметр | минорном выпуске, например 1.1.0 |
| Удалённый или переименованный член, изменение сигнатуры или описанного в документации поведения | мажорном выпуске, например 2.0.0 |
| Новое значение перечисления или новый подкласс запечатанного класса | мажорном выпуске, например 2.0.0 |
С версии 1.0.0 async_job следует семантическому версионированию: ограничение
^1.0.0 принимает каждый минорный и патч-выпуск, и код, написанный под 1.0.0,
с ними продолжает работать, кроме случаев ниже. Публичный API — то, что
экспортирует package:async_job/async_job.dart; текст сообщений об ошибках
и отладочного вывода в него не входит. Член, который уходит, сначала помечается
@Deprecated в минорном выпуске, с именем, которое брать вместо него,
и удаляется в следующем мажорном.
Минорный выпуск может добавить член в класс, от которого вы наследуетесь,
например в JobObserver, и одноимённый член в вашем коде после этого перестаёт
компилироваться. Он может добавить член и в интерфейс, который вы реализуете
ради мока, например в Job или JobContext, и тогда перестаёт компилироваться
и фейк, который реализует его вручную, а не через noSuchMethod. Список
изменений такого выпуска называет члены, которые он добавляет. Новое имя
верхнего уровня может столкнуться с тем же именем, импортированным из другого
пакета, пока один из импортов не скроет его через hide.
switch по Outcome или по JobErrorAnswer может быть исчерпывающим, и новый
подкласс запечатанного Outcome или новое значение перечисления сломали бы его
компиляцию: такие правки выходят только в мажорном выпуске. CancelReason
открыт, и минорный выпуск может добавить причину, поэтому switch по причинам
оставляет ветку по умолчанию.
То, что добавляет package:async_job/engine.dart, — протокол для построения
движка — в обещание не входит: JobBase, JobContextBase, JobStatus и их
защищённые члены могут измениться в минорном выпуске, и список изменений этого
выпуска говорит как, в разделе «Протокол движка». Движок держит async_job
в пределах одной минорной версии, например >=1.0.0 <1.1.0, и выходит с каждым
его минорным выпуском; Своё поверх ядра
говорит то же.
solo добавляет состояние, очередь
и декларативные правила. Он выполняет одну корневую задачу за раз и даёт ей
исключительный доступ к состоянию. Используйте его, если нужен контроллер
с такими гарантиями. Он реэкспортирует async_job, поэтому достаточно
зависимости от solo. Код на solo называет вместо async_job_lints
solo_lints: тот включает правила
async_job_lints и добавляет правила контроллера.
Задачи работают везде, где работает Dart. Чтобы связать solo с виджетами,
используйте flutter_solo.