$ sudo teach|
    $ sudo teach|
    IT school
  • Telegram
  • Партнёрам
  • Все курсы
$ sudo teach IT
OOO "SALEPROFIT"Контакты и реквизитыIT-Park Logo

Школа

  • Блог
  • Проверить сертификат

Сотрудничество

  • Стать учителем
  • Партнёрская программа
  • О проекте

Право

  • Оферта
  • Политика конфиденциальности

© 2023–2026 $ sudo teach IT™. All Rights Reserved. Public user contributions licensed under CC BY-SA 4.0 license with attribution required
TelegramGitHubYouTube
ГлавнаяБлогDart: асинхронность для прикладных задач — Future, Stream и отмена

Dart: асинхронность для прикладных задач — Future, Stream и отмена

$ sudo teach IT
·26 июля 2026 г.·10 мин·69
Dart: асинхронность для прикладных задач — Future, Stream и отмена

Разберём разницу Future/Stream, как правильно собирать результаты из нескольких запросов и когда нужна отмена. На примерах покажем, как избегать подвисаний и гонок.

Содержание
Future vs Stream: что означает модель данныхFuture<T> — один результат в будущемStream<T> — поток значений во времениКак правильно комбинировать несколько запросовСценарий 1: параллельно получить несколько Future и собрать результатСценарий 2: собрать результаты «как только готово» — и не блокировать UIСценарий 3: гонки (race conditions) при повторных запросахОтмена (cancellation): когда она нужна и как делать правильноПроблема: «отменить Future» напрямую нельзя (в общем случае)Отмена для Stream: отмена подписки как базовая практикаОтмена для Future: токен отмены и безопасные точкиВариант 1: отмена как «не применяй результат»Вариант 2: отмена с StreamController и жизненным цикломКогда нужна отмена, а когда достаточно request idОтмена нужна, если:Достаточно request id, если:Пример: безопасный поиск с дебаунсом, гонками и отменойПодвисания и частые ошибки: что действительно ломает приложениеОшибка 1: «await внутри цикла» вместо параллелизмаОшибка 2: отсутствие лимитов параллельностиОшибка 3: обновление состояния после ухода со страницыОшибка 4: неправильное объединение Stream-событийКак

Асинхронность в Dart — не абстрактная «фича ради фичи», а практический механизм, который определяет качество UI, надёжность сетевых интеграций и стабильность фоновых вычислений. На практике разработчики чаще всего сталкиваются с тремя вопросами:

  1. Что выбрать: Future или Stream?
  2. Как правильно собирать результаты из нескольких запросов, не получая гонки и непредсказуемые состояния?
  3. Когда и как нужна отмена (cancellation), чтобы не подвисать и не тратить ресурсы?

Ниже — разбор с упором на прикладные сценарии: загрузки данных, объединение нескольких источников, обработка последовательностей событий и управление жизненным циклом запросов.


Future vs Stream: что означает модель данных

Future<T> — один результат в будущем

Future описывает вычисление, которое завершится один раз — либо успешным значением типа T, либо ошибкой.

Примеры:

  • HTTP-запрос, который возвращает JSON один ответ;
  • чтение файла: результат появится один раз;
  • вычисление, которое выдаёт итог.

Ключевые свойства:

  • У Future нет «набора» значений. Есть ровно один финал.
  • Вы «подписываетесь» через await, .then, или обработку ошибки.
  • Отменить сам Future обычно нельзя напрямую (важно для отмены — ниже).

Пример: простой запрос и ожидание:

code
Future<User> fetchUser() async {
  final response = await http.get(Uri.parse('https://example.com/user'));
  if (response.statusCode != 200) {
    throw Exception('Bad status: ${response.statusCode}');
  }
  return User.fromJson(jsonDecode(response.body));
}

void example() async {
  try {
    final user = await fetchUser();
    print(user.name);
  } catch (e) {
    print('Ошибка: $e');
  }
}

Stream<T> — поток значений во времени

Stream — это последовательность значений, которая может:

  • выдавать много T во времени;
  • завершиться нормально;
  • завершиться с ошибкой;
  • продолжаться долго (например, WebSocket, события UI).

Типовые сценарии:

  • события формы: каждый ввод;
  • позиция/прогресс загрузки;
  • события из WebSocket или SSE;
  • повторяемые опросы или «тикеры».

Ключевые свойства:

  • Поток имеет жизненный цикл: подписка → обработка → отмена/закрытие.
  • Отмена обычно происходит через управление подпиской (cancel()).
  • Stream — лучший выбор, когда результат не «один раз», а происходит во времени.

Минимальный пример потока:

code
Stream<int> counter() async* {
  for (var i = 0; i < 5; i++) {
    await Future<void>.delayed(const Duration(milliseconds: 200));
    yield i;
  }
}

Как правильно комбинировать несколько запросов

Одна из самых частых причин багов — попытка «на глаз» ждать несколько асинхронных операций, смешивая await в неправильном порядке и не контролируя конкурентность. Dart предоставляет инструменты, но их нужно применять в соответствии с моделью Future/Stream.

Сценарий 1: параллельно получить несколько Future и собрать результат

Если вам нужно запросить несколько ресурсов одновременно и дождаться всех — используйте Future.wait.

Пусть есть три независимых запроса:

  • профиль пользователя;
  • список избранного;
  • настройки.
code
Future<Profile> loadProfile() async => /* ... */;
Future<List<Article>> loadFavorites() async => /* ... */;
Future<Settings> loadSettings() async => /* ... */;

Future<void> loadDashboard() async {
  // Параллельный старт запросов
  final futures = <Future>[
    loadProfile(),
    loadFavorites(),
    loadSettings(),
  ];

  // Ожидаем все и получаем список результатов
  final results = await Future.wait(futures);

  final profile = results[0] as Profile;
  final favorites = results[1] as List<Article>;
  final settings = results[2] as Settings;

  // Далее — обновление состояния
  print(profile.userName);
  print('favorites: ${favorites.length}');
}

Важные нюансы Future.wait

  1. Если один Future упадёт, упадёт всё (поведение по умолчанию).
  2. Можно настроить частичное продолжение с eagerError (аккуратно: вам всё равно придётся продумать логику).
  3. Future.wait запускает все futures сразу, но порядок results соответствует порядку в списке futures — не забывайте про это при сборке.

Если вам важны типы и читаемость, лучше использовать обобщённую упаковку и явные ожидания:

code
Future<DashboardData> loadDashboard() async {
  final profileF = loadProfile();
  final favoritesF = loadFavorites();
  final settingsF = loadSettings();

  final results = await Future.wait([
    profileF,
    favoritesF,
    settingsF,
  ]);

  return DashboardData(
    profile: results[0] as Profile,
    favorites: results[1] as List<Article>,
    settings: results[2] as Settings,
  );
}

Сценарий 2: собрать результаты «как только готово» — и не блокировать UI

Иногда хочется не ждать всех, а по мере завершения обновлять состояние. Для Future это делается через комбинации:

  • запуск всех запросов,
  • обработка каждого Future через then/await в отдельных ветках,
  • агрегация через Completer или StreamController (часто удобнее сразу строить Stream).

Например, вы запускаете три запроса, и по мере готовности заполняете модель:

code
class DashboardParts {
  final Profile? profile;
  final List<Article>? favorites;
  final Settings? settings;

  const DashboardParts({
    this.profile,
    this.favorites,
    this.settings,
  });

  DashboardParts copyWith({
    Profile? profile,
    List<Article>? favorites,
    Settings? settings,
  }) {
    return DashboardParts(
      profile: profile ?? this.profile,
      favorites: favorites ?? this.favorites,
      settings: settings ?? this.settings,
    );
  }
}

Future<Stream<DashboardParts>> loadDashboardIncremental() async {
  final controller = StreamController<DashboardParts>();
  var state = const DashboardParts();

  Future<void> setProfile() async {
    final profile = await loadProfile();
    state = state.copyWith(profile: profile);
    controller.add(state);
  }

  Future<void> setFavorites() async {
    final favorites = await loadFavorites();
    state = state.copyWith(favorites: favorites);
    controller.add(state);
  }

  Future<void> setSettings() async {
    final settings = await loadSettings();
    state = state.copyWith(settings: settings);
    controller.add(state);
  }

  // Запускаем параллельно
  unawaited(Future.wait([
    setProfile(),
    setFavorites(),
    setSettings(),
  ]).then((_) => controller.close()).catchError((e, st) {
    controller.addError(e, st);
    controller.close();
  }));

  return controller.stream;
}

Это один из случаев, когда переход от чистого Future к Stream логически оправдан: вы описываете изменения во времени.

Сценарий 3: гонки (race conditions) при повторных запросах

Классический баг: пользователь вводит текст или экран меняет параметр запроса, и старый запрос приходит позже — перетирает новое состояние.

Пример проблемного кода (упрощённо):

code
String query = '';

Future<void> onQueryChanged(String newQuery) async {
  query = newQuery;

  final results = await search(newQuery); // ждём долго
  // Если за время ожидания query изменился — мы перезапишем UI устаревшими данными
  showResults(results);
}

Антидот: идентификатор запроса (request id)

Самый простой и надёжный подход — привязать результат к «версии» запроса:

code
int _requestId = 0;

Future<void> onQueryChanged(String newQuery) async {
  final id = ++_requestId;

  final results = await search(newQuery);

  // Игнорируем устаревший ответ
  if (id != _requestId) return;

  showResults(results);
}

Плюсы:

  • не требует отмены нижнего транспорта;
  • работает и для Future, и для операций, где отмена не реализована.

Минусы:

  • ресурсы на старый запрос могут быть потрачены (но UI не сломается).

Антидот 2: stream-операторы (debounce/distinct) и cancel

Если события идут как поток (например, текстовое поле в форме), то Stream помогает структурно:

  • debounceTime — не запускать запрос после каждого символа;
  • distinct — игнорировать одинаковые значения;
  • switchMap/«переключение» — отменять предыдущую ветку (в библиотечных пакетах) и слушать только последнюю.

В чистом Dart без готового switchMap иногда делают через подписки и cancel (ниже увидите механику отмены).


Отмена (cancellation): когда она нужна и как делать правильно

Проблема: «отменить Future» напрямую нельзя (в общем случае)

В Dart нет универсального способа отменить произвольный Future, потому что Future — это контракт «когда-то завершится», а не управляемая вычислительная задача.

Однако отмена может быть организована на уровне:

  1. сервиса/клиента (HTTP клиент поддерживает abort),
  2. вашей логики (проверка флага/токена),
  3. stream-подписок (отмена через cancel() работает естественно).

Поэтому говорить «отмена» нужно конкретно: отменяем результат/подписку? отменяем сетевой запрос? предотвращаем обновление UI?


Отмена для Stream: отмена подписки как базовая практика

Если вы подписываетесь на поток, то отмена обычно делается так:

code
late final StreamSubscription<int> sub;

void start() {
  sub = counter().listen(
    (value) => print('value=$value'),
    onError: (e, st) => print('error=$e'),
    onDone: () => print('done'),
  );
}

Future<void> stop() async {
  await sub.cancel(); // корректно освобождает ресурсы подписки
}

Это особенно важно в UI: при уходе со страницы вы должны отменить подписку, иначе обработчики будут выполняться «после смерти виджета», и вы получите утечки/искажения состояния.


Отмена для Future: токен отмены и безопасные точки

Для прикладных задач (поиск, автозаполнение, загрузка карточек по маршруту) обычно делают комбинацию:

  • токен отмены (cancel token / request id / abort controller),
  • проверка токена перед применением результата,
  • (желательно) отмена сетевого запроса.

Вариант 1: отмена как «не применяй результат»

Это самый практичный путь для большинства проектов. Например, вы запускаете запрос при смене параметра, но даже если отменить транспорт нельзя, вы отменяете эффект.

code
class CancelToken {
  bool _canceled = false;
  bool get canceled => _canceled;
  void cancel() => _canceled = true;
}

Future<void> exampleWithCancelToken(CancelToken token) async {
  final results = await search('abc');
  if (token.canceled) return; // не применяем
  showResults(results);
}

Дальше в UI/сервисе:

code
CancelToken? _token;

Future<void> onNewQuery(String q) async {
  // отменяем прошлую операцию по смыслу
  _token?.cancel();
  final token = CancelToken();
  _token = token;

  final localToken = token;
  await exampleWithCancelToken(localToken);
}

Это не экономит сетевой трафик, но предотвращает гонки и «подвисшие» состояния в UI.

Вариант 2: отмена с StreamController и жизненным циклом

Иногда вы строите асинхронный конвейер как Stream, и отмена делается естественно: вы прекращаете слушать и закрываете контроллер.

code
Stream<List<Article>> searchStream(String query) async* {
  // Пример: имитация пагинации
  int page = 1;
  while (true) {
    final chunk = await fetchPage(query, page);
    if (chunk.isEmpty) break;
    yield chunk;
    page++;
  }
}

Потребитель может остановить всё простой cancel подписки.

Если вам критична отмена «в середине», важно, чтобы источник реально проверял состояние/отмену. В чистом примере выше это делается через await и цикл — но реальный транспорт может зависеть от сети.


Когда нужна отмена, а когда достаточно request id

Рассмотрим критерии.

Отмена нужна, если:

  • операция дорогая (трафик, батарея, CPU);
  • запрос может длиться долго (пейджинг, трансляции, большие отчёты);
  • пользователь может часто инициировать смену контекста (поиск по символам, навигация);
  • вы хотите освобождать ресурсы не только на уровне UI.

Достаточно request id, если:

  • отмена сложна или недоступна (нет abort API на уровне HTTP клиента/сервиса);
  • главное — не допустить «старые данные сверху»;
  • операция в среднем короткая, а при отмене вы теряете мало.

На практике часто делают так: request id + (опционально) abort. Это обеспечивает и корректность, и экономию ресурсов там, где возможно.


Пример: безопасный поиск с дебаунсом, гонками и отменой

Представим: есть поток ввода текста (например, из UI), запускается поиск. Нужны:

  • debounceTime, чтобы не дергать сервер на каждый символ;
  • предотвращение гонок (устаревшие ответы не должны отображаться);
  • отмена предыдущего сетевого запроса там, где возможно.

Ниже — концептуальный вариант. Поскольку в стандартной библиотеке нет готового switchMap, сделаем вручную через токен и подписку.

code
import 'dart:async';

class CancelToken {
  bool _canceled = false;
  bool get canceled => _canceled;
  void cancel() => _canceled = true;
}

Future<List<String>> search(String q) async {
  // заглушка: имитация сети
  await Future<void>.delayed(const Duration(milliseconds: 500));
  if (q.length < 2) return const [];
  return List.generate(3, (i) => '$q result $i');
}

Future<void> runSearchPipeline(Stream<String> queryChanges) async {
  CancelToken? current;
  StreamSubscription<String>? sub;

  // состояние для дебаунса
  Timer? debounceTimer;

  sub = queryChanges.listen((q) {
    debounceTimer?.cancel();

    debounceTimer = Timer(const Duration(milliseconds: 300), () async {
      // отменяем предыдущую операцию по смыслу
      current?.cancel();
      final token = CancelToken();
      current = token;

      final queryAtStart = q;
      try {
        final results = await search(queryAtStart);

        if (token.canceled) return; // гонка или отмена
        // Обновляем UI/состояние только актуального запроса
        print('Results for "$queryAtStart": $results');
      } catch (e) {
        if (token.canceled) return;
        print('Search error: $e');
      }
    });
  });

  // где-то в жизненном цикле:
  // await sub.cancel();
}

Этот пример демонстрирует важную мысль: антигонки и отмена — это не только про отмену сети, а про управление тем, какие результаты допустимы к применению.


Подвисания и частые ошибки: что действительно ломает приложение

Ошибка 1: «await внутри цикла» вместо параллелизма

Например, вы делаете N запросов и ожидаете каждый последовательно:

code
for (final id in ids) {
  final item = await fetchItem(id); // последовательно
  items.add(item);
}

Если запросы независимы, вы получите лишнюю задержку. Правильнее параллелить:

code
final items = await Future.wait(ids.map(fetchItem));

Исключение — если нужно ограничить параллелизм (например, API rate limits).

Ошибка 2: отсутствие лимитов параллельности

Future.wait запускает всё сразу. Для сотен id это может:

  • уронить производительность,
  • вызвать throttling,
  • создать лавину ошибок.

Тогда нужен контроллер конкурентности (очередь). В статье не разворачиваем реализацию, но принцип такой: вы запускаете не все N сразу, а, скажем, по 5–10 одновременно.

Ошибка 3: обновление состояния после ухода со страницы

Типичная причина утечек и «странных» ошибок: вы подписались на Stream или запустили Future, а потом пользователь ушёл. Без отмены результат продолжает приходить и пытается менять состояние.

Лечение:

  • для Stream — subscription.cancel() в dispose;
  • для Future — проверка mounted (в Flutter), request id или cancel token.

Ошибка 4: неправильное объединение Stream-событий

Если ваш поток генерирует события быстро, обработчик может не успевать. Тогда возможны:

  • накопление очереди,
  • «запаздывание» UI,
  • рост памяти.

Нужно продумывать стратегию:

  • дебаунс/троттлинг на входе,
  • ограничение скорости обработки,
  • буферизацию или пропуск промежуточных состояний.

Как

Войдите, чтобы поставить лайк и оставить комментарий.

Автор

$ sudo teach IT

Курс по теме

Dart Free

Язык программирования Dart и бесплатный курс разработки кроссплатформенных приложений

Открыть курс

Продолжите обучение

Все курсы
Python – для начинающих!

Python – для начинающих!

С нуля до профессионального уровня. Подходит для всех. Учитесь каждый день и овладейте самым популярным языком программирования.

Перейти к курсу
Solidity DLC - поймай, если сможешь

Solidity DLC - поймай, если сможешь

Аудит смарт-контрактов на реальных взломах. The DAO, Parity, Wormhole, Ronin, Euler и ещё 7 разборов: что пошло не так, сколько потеряли и что бы спасло. Атаки и защиты на Foundry, план входа в профессию аудитора.

Перейти к курсу
Ботостроение Telegram

Ботостроение Telegram

Лёгкий, быстрый и доступный способ познакомиться с миром ботостроения в Telegram. Видео, конспекты, практика и помощь – всё у нас на курсе.

Перейти к курсу
Solidity pro - всё или ничего

Solidity pro - всё или ничего

Как пишут контракты, которые работают в основной сети. Наследование и OpenZeppelin, паттерны продакшена, оптимизация газа, ABI, delegatecall и подписи. 7 модулей, 33 урока, а в финале собственное dApp: мини-DAO с токеном, голосованием и казной.

Перейти к курсу

Другие статьи

Работа с Gantt-логикой задач: как проектировать дедлайны, ретраи и ограничения в асинхронных сервисах
работа

Работа с Gantt-логикой задач: как проектировать дедлайны, ретраи и ограничения в асинхронных сервисах

Покажем практический шаблон управления временем выполнения: дедлайн на весь запрос, отмена вложенных задач, ретраи с backoff и защита от каскадных зависаний при сетевых ошибках.

17 сентября 2026 г.
1840
FastAPI и фоновые задачи: когда лучше использовать BackgroundTasks, а когда очередь
fastapi

FastAPI и фоновые задачи: когда лучше использовать BackgroundTasks, а когда очередь

Поймём различия между синхронной обработкой, BackgroundTasks и внешними очередями. Разберём idempotency, ретраи и мониторинг фоновых процессов.

22 июля 2026 г.
1150
Как выучить программирование самому и бесплатно: с чего начать и в каком порядке
выучить

Как выучить программирование самому и бесплатно: с чего начать и в каком порядке

Разберём понятный маршрут для новичка: как выбрать первый язык, что учить в первую очередь, какие мини-проекты делать и как не забросить занятия. Дам бесплатные ориентиры и план, чтобы знания складывались в портфолио.

21 сентября 2026 г.
1950
Что такое HTML и чем он отличается от CSS — объясню простыми словами и на примерах
такое

Что такое HTML и чем он отличается от CSS — объясню простыми словами и на примерах

Разберём, из чего состоит веб-страница: что делает HTML (смысл и структура) и что делает CSS (оформление). В конце соберёте мини-страницу и поймёте, что нужно учить в каком порядке.

30 октября 2026 г.
100
Cargo для продакшена: workspaces, фичи и стратегия версий зависимостей
cargo

Cargo для продакшена: workspaces, фичи и стратегия версий зависимостей

Разберём, как организовать Rust workspace, управлять optional features, фиксировать версии библиотек и уменьшать “дрейф” зависимостей при командной разработке и CI.

18 сентября 2026 г.
1940
Что такое SQL и зачем он нужен новичку: как думать о данных простыми словами
такое

Что такое SQL и зачем он нужен новичку: как думать о данных простыми словами

Разберём, что такое базы данных и SQL без сложной теории: как выбирать данные, фильтровать их и собирать таблицы в отчёт. В конце — короткий план, как потренироваться и собрать первые запросы для портфолио.

28 октября 2026 г.
220

Комментарии

Станьте первым в обсуждении

Поделитесь мнением о статье или задайте вопрос автору

Содержание

Future vs Stream: что означает модель данныхFuture<T> — один результат в будущемStream<T> — поток значений во времениКак правильно комбинировать несколько запросовСценарий 1: параллельно получить несколько Future и собрать результатСценарий 2: собрать результаты «как только готово» — и не блокировать UIСценарий 3: гонки (race conditions) при повторных запросахОтмена (cancellation): когда она нужна и как делать правильноПроблема: «отменить Future» напрямую нельзя (в общем случае)Отмена для Stream: отмена подписки как базовая практикаОтмена для Future: токен отмены и безопасные точкиВариант 1: отмена как «не применяй результат»Вариант 2: отмена с StreamController и жизненным цикломКогда нужна отмена, а когда достаточно request idОтмена нужна, если:Достаточно request id, если:Пример: безопасный поиск с дебаунсом, гонками и отменойПодвисания и частые ошибки: что действительно ломает приложениеОшибка 1: «await внутри цикла» вместо параллелизмаОшибка 2: отсутствие лимитов параллельностиОшибка 3: обновление состояния после ухода со страницыОшибка 4: неправильное объединение Stream-событийКак