Урок по треку Flutter и Dart 8 мин

Dart: event loop, Future, Stream и изоляты

Event loop и microtask, async/await, Stream, изоляты и compute.

Код Dart выполняется в изоляте: у него своя память и один поток. Внутри изолята работает event loop с двумя очередями. Понимание их порядка нужно и для задач «что выведет», и для отладки зависаний интерфейса.

Две очереди

  • Microtask queue: scheduleMicrotask, Future.microtask, колбэки then у уже завершённого Future, продолжение после await.
  • Event queue: таймеры (Future(...), Future.delayed, Timer), ввод, ответы ввода-вывода, сообщения от других изолятов.

Цикл: выполнить синхронный код до конца, затем все микрозадачи, затем одно событие, снова все микрозадачи, и так далее.

void main() {
  print('1');
  Future(() => print('2'));
  scheduleMicrotask(() => print('3'));
  Future.value(4).then(print);
  print('5');
}
// 1 5 3 4 2

Кадры Flutter тоже приходят через event loop. Пока работает долгий синхронный код или бесконечная цепочка микрозадач, кадр не отрисуется, и интерфейс подвиснет.

Future и async/await

Future<T> это значение, которое появится позже, или ошибка. async-функция всегда возвращает Future и выполняется синхронно до первого await. После await продолжение планируется асинхронно, даже если ожидаемое значение уже готово.

Future<void> load() async {
  print('A');
  await null;      // всё равно отдаёт управление
  print('B');
}
void main() {
  load();
  print('C');      // A C B
}

Ошибки в async-функции превращаются в завершённый ошибкой Future. Ловятся через try/catch вокруг await или catchError. Future без await и без обработчика ошибки даст необработанную ошибку зоны. Если результат намеренно не ждём, пишут unawaited(f()), а линтер unawaited_futures напоминает о забытых await.

Параллельно: await Future.wait([a(), b()]) запускает оба сразу. Последовательное await a(); await b(); в два раза дольше, если задачи независимы. С Dart 3 можно ждать record: final (u, p) = await (getUser(), getPosts()).wait;.

Stream

Stream это последовательность асинхронных событий: данные, ошибки и завершение.

  • Single-subscription (по умолчанию): один слушатель, события буферизуются, пока слушателя нет. Пример: чтение файла, HTTP-ответ.
  • Broadcast: много слушателей, события без слушателя теряются. Пример: события кликов, шина событий.
final sub = stream.listen(onData, onError: onError, onDone: onDone);
await sub.cancel();   // обязательно в dispose

async* с yield создаёт Stream, await for его читает. StreamController даёт ручное управление: add, addError, close. Незакрытые контроллеры и неотменённые подписки дают утечки памяти и вызовы setState после dispose.

Изоляты

Изоляты не делят память, общаются сообщениями через порты. Поэтому в Dart нет гонок данных на общих объектах, но передача больших данных стоит копирования.

final result = await Isolate.run(() => parseHugeJson(text));
// во Flutter то же самое: compute(parseHugeJson, text)

Когда нужен изолят: тяжёлые вычисления больше нескольких миллисекунд, парсинг большого JSON, обработка изображений. Сетевые запросы и чтение файлов в изолят выносить не нужно: это ожидание, а не работа CPU, оно и так не блокирует поток.

Для долгоживущего фонового воркера используют Isolate.spawn и пару ReceivePort/SendPort. В фоновом изоляте можно звать платформенные каналы после BackgroundIsolateBinaryMessenger.ensureInitialized.

Частые ошибки

  • Думать, что async сразу уводит выполнение в фон: код до первого await синхронный.
  • Тяжёлый цикл в async-функции: он всё равно блокирует UI.
  • Забытый await и молча проглоченная ошибка.
  • listen без cancel в dispose.

Что спросят на собесе

  • Порядок microtask и event queue, задачи на вывод.
  • Чем Future отличается от Stream, single-subscription от broadcast.
  • Как распараллелить независимые запросы.
  • Что такое изолят, чем отличается от потока, когда нужен compute.
  • Почему async не спасает от фризов при тяжёлых вычислениях.

Закрепите за 5 минут

После урока в тренажёре 3-5 проверочных вопросов, дальше модуль уходит в повторения.