Перейти к содержимому

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 opened
database closed
cancelled: manual

ctx.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 sent
Cancelled(manual)
quote answered

ctx.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.