4.8. Продвинутая асинхронность: Future

В начале пути Flutter-разработчика вам кажется, что Dart — это простой язык (и это действительно так!), но при масштабировании приложений возникают неожиданные ситуации с гонками состояний, подвисаниями интерфейса при идеальной вёрстке и прочие проблемы, которые сложно отловить.

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

Цель этого и трёх последующих параграфов — не просто разобрать абстракции Dart для асинхронного программирования, а показать, как они создаются, завершаются и доставляют результат. Всё это даст вам знания, которые помогут предотвратить непредсказуемое поведение асинхронного кода.

Мы детально рассмотрим четыре темы:

  • Future

  • Zone

  • Микротаски (microtask)

  • Инструменты для управления асинхронными операциями

В этом параграфе сфокусируемся на Future. Разберём, что это за абстракция в Dart, как устроен её жизненный цикл, какие механизмы лежат в основе её работы, какие ограничения накладывает асинхронная модель программирования и как правильно работать с Future, чтобы избежать распространённых ошибок и проблем с архитектурой.

{% note info "Важно" %}
Важно
Для уверенного осмысления концепций советуем освежить знания из первого параграфа про асинхронность.

Особенно про определение асинхронных операций, обработку ошибок и устройство очереди Event Loop.

Начинаем!

Как устроена жизнь Future

В Dart объект Future представляет собой асинхронную операцию, которая может завершиться с результатом или ошибкой. Жизненный цикл Future тесно связан с Event Loop (циклом событий) и управляется средой выполнения Dart. Сейчас мы подробно рассмотрим, как работает Future: разберём процесс его создания, планирование исполнения, доставку результата и другие жизненные циклы. Начнём с вызова конструктора Future и пошагово проследим, как объект Future взаимодействует с Event Loop и другими механизмами Dart.

Вызов конструктора Future

Путь создания Future начинается с конструктора. Мы постепенно пройдём все этапы, погружаясь дальше. Начнём с кода конструктора:

1  factory Future(FutureOr<T> computation()) {
2    _Future<T> result = new _Future<T>();
3    Timer.run(() {
4      try {
5        result._complete(computation());
6      } catch (e, s) {
7        _completeWithErrorCallback(result, e, s);
8      }
9    });
10    return result;
11  }

Создаётся объект _Future, в который будет доставлен результат или ошибка по результатам работы Timer.

Функция Timer.run создаёт Timer с нулевой длительностью:

1  static void run(void Function() callback) {
2    new Timer(Duration.zero, callback);
3  }

В конструкторе Timer происходит обращение к текущему контексту исполнения Zone и создание таймера в ней, где впоследствии будет вызван метод:

1  factory Timer(Duration duration, void Function() callback) {
2    if (Zone.current == Zone.root) {
3      // No need to bind the callback. We know that the root's timer will
4      // be invoked in the root zone.
5      return Zone.current.createTimer(duration, callback);
6    }
7    return Zone.current
8        .createTimer(duration, Zone.current.bindCallbackGuarded(callback));
9  }
10
11  external static Timer _createTimer(Duration duration, void Function() callback);

Ключевое слово external означает, что реализация функции находится за пределами Dart и предоставляется с помощью интерфейса FFI. Управление передано виртуальной машине DartVM, которая зарегистрирует наш колбэк в Event Loop и вызовет через указанный промежуток времени.

Интерфейс FFI

Расшифровывается как Foreign Function Interface. Эта сущность позволяет вызывать код библиотек на других языках, например C.

В нашем случае он равен нулю, то есть вызов произойдёт в следующей итерации цикла. Получается, что разница между Future() и Future.delayed() — только в значении времени, которое передаётся в конструктор. Все Future в Dart являются delayed.

Жизненный цикл Future в Dart можно представить так:

  1. Создание и захват контекста. Future создаётся, захватывая текущую Zone, что позволяет перехватывать ошибки и управлять асинхронным поведением, контролируя методы createTimer(), scheduleMicrotask().

  2. Планирование исполнения. Планирование зависит от способа завершения Future и делится на два основных пути:

    • Асинхронный (стандартный) путь. При использовании стандартного Completer или конструкторов вроде Future.value завершение планируется в очереди микротасок. Это гарантирует, что вызовы в .then будут выполнены позже, в отдельном цикле обработки микротасок, а не в текущем стеке вызовов.

    • Синхронный (особый) путь. При использовании Completer.sync() и его завершении, исполнение происходит немедленно и синхронно. В этом случае обратные вызовы из .then выполняются сразу же, в том же стеке вызовов, минуя очередь микротасок.

  3. Передача управления. Виртуальная машина DartVM управляет очередями задач (микротаски и Event Queue) и выполняет запланированные задачи в определённом порядке.

  4. Доставка результата. Даже если Future уже завершён (через Future.value() или Future.sync()), результат передаётся через очередь микротасок, что предотвращает проблемы повторного входа и обеспечивает единообразное асинхронное поведение.

  5. Сборка мусора. Если Future больше не используется, он удаляется сборщиком мусора.

Как видим, Future — это абстракция над Timer, которая даёт удобный синтаксис для создания последовательности асинхронных операций с помощью ключевого слова await или функции .then(), а также для обработки ошибок, возникающих в процессе исполнения, с помощью try/catch или функции .catchError().

Конструктор Future.microtask()

Если был вызван конструктор Future.microtask(), то выполнение будет отличаться. Давайте посмотрим на код.

1  factory Future.microtask(FutureOr<T> computation()) {
2    _Future<T> result = new _Future<T>();
3    scheduleMicrotask(() {
4      try {
5        result._complete(computation());
6      } catch (e, s) {
7        _completeWithErrorCallback(result, e, s);
8      }
9    });
10    return result;
11  }

В метод scheduleMicrotask(), так же как и в таймер, передаётся колбэк, который призван выполнить вычисление и завершить Future этим результатом или ошибкой. Внутри scheduleMicrotask() также происходит проверка Zone и захват контекста исполнения.

В документации к методу scheduleMicrotask() написано предупреждение с примером кода, когда с помощью Timer и микротасок можно спровоцировать что-то похожее на зависание приложения. Другими словами, ваши таймеры никогда не будут выполнены:

1/// main() {
2///   Timer.run(() { print("executed"); }); // Will never be executed.
3///   foo() {
4///     scheduleMicrotask(foo); // Schedules [foo] in front of other events.
5///   }
6///   foo();
7/// }

Регистрируется таймер, а затем вызывается локальная функция foo(), которая запланирует микротаску с такой же функцией.

Давайте изучим код регистрации передаваемого колбэка:

1/// Schedules a callback to be called as a microtask.
2///
3/// The microtask is called after all other currently scheduled
4/// microtasks, but as part of the current system event.
5void _scheduleAsyncCallback(_AsyncCallback callback) {
6  _AsyncCallbackEntry newEntry = new _AsyncCallbackEntry(callback);
7  _AsyncCallbackEntry? lastCallback = _lastCallback;
8  if (lastCallback == null) {
9    _nextCallback = _lastCallback = newEntry;
10    if (!_isInCallbackLoop) {
11      _AsyncRun._scheduleImmediate(_startMicrotaskLoop);
12    }
13  } else {
14    lastCallback.next = newEntry;
15    _lastCallback = newEntry;
16  }
17}
18

Метод _scheduleAsyncCallback вызывается зоной после захвата контекста исполнения. Далее формируется связанный список из _AsyncCallbackEntry и происходит проверка:

  • если мы не закончили исполнение текущего цикла Microtask Loop и у нас остались lastCallback, то надо добавить следующий в текущую же итерацию;

  • если же мы закончили исполнение Microtask Loop, то задачи уйдут на следующую итерацию Event Loop.

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

Код _AsyncRun._scheduleImmediate(_startMicrotaskLoop); отвечает за сигнал DartVM, что появилась микротаска и надо начать следующую итерацию.

1void _startMicrotaskLoop() {
2  _isInCallbackLoop = true;
3  try {
4    // Moved to separate function because try-finally prevents
5    // good optimization.
6    _microtaskLoop();
7  } finally {
8    _lastPriorityCallback = null;
9    _isInCallbackLoop = false;
10    if (_nextCallback != null) {
11      _AsyncRun._scheduleImmediate(_startMicrotaskLoop);
12    }
13  }
14}
15
16void _microtaskLoop() {
17  for (var entry = _nextCallback; entry != null; entry = _nextCallback) {
18    _lastPriorityCallback = null;
19    var next = entry.next;
20    _nextCallback = next;
21    if (next == null) _lastCallback = null;
22    (entry.callback)();
23  }
24}

Функция _microtaskLoop() будет исполнять синхронный цикл до тех пор, пока не закончатся колбэки для исполнения. Это итерация по связанному списку, который может пополняться в процессе исполнения.

Планирование микротасок внутри текущего цикла приведёт к зависаниям

Планирование микротасок внутри текущего цикла приведёт к зависаниям

Планирование новых микротасок внутри уже исполняющейся микротаски может привести к бесконечному связанному списку операций, из-за которых ваши Future и Timer никогда не будут выполнены. А в случае Flutter перестанут отрисовываться кадры, так как сигнал об отрисовке кадра тоже находится в EventQueue или в очереди Future.

Конструкторы Future.sync() и Future.value()

Иногда нужно, чтобы функция всегда возвращала Future, даже если её результат можно получить синхронно. Future.sync() позволяет выполнить код синхронно и вернуть результат c помощью Future.value(), последнее можно использовать, когда результат уже известен и вычисление не нужно.

1  factory Future.sync(FutureOr<T> computation()) {
2    try {
3      var result = computation();
4      return result is Future<T> ? result : _Future<T>.value(result);
5    } catch (error, stackTrace) {
6      var future = new _Future<T>();
7      AsyncError? replacement = Zone.current.errorCallback(error, stackTrace);
8      if (replacement != null) {
9        future._asyncCompleteError(replacement.error, replacement.stackTrace);
10      } else {
11        future._asyncCompleteError(error, stackTrace);
12      }
13      return future;
14    }
15  }
  • В блоке try вызывается функция computation(), и её результат сохраняется в переменной result.

  • Если result — это Future<T>, он возвращается как есть, иначе он оборачивается в _Future<T>.value(result), чтобы создать Future, который завершается с этим значением.

  • Если в процессе выполнения computation возникает исключение, оно перехватывается блоком catch:

    • Вызывается Zone.current.errorCallback, чтобы позволить зоне обработать ошибку.
    • Незавершённый Future завершается с ошибкой с помощью метода _asyncCompleteError.

В данном случае вычисление выполняется синхронно, и, когда не возникает ошибки, возвращается результат с помощью _Future<T>.value(result). А в случае ошибки сначала происходит обращение к текущей зоне обработать ошибку. Зона перехватывает ошибку и может изменить её. А потом завершает Future с помощью функции _asyncCompleteError(). Если пройти по коду _Future<T>.value(result), то можно увидеть, что значение там тоже доставляется с помощью похожей функции — _asyncCompleteWithValue().

Давайте посмотрим на реализацию _asyncCompleteWithValue(). Держим в уме, что _asyncCompleteError() выглядит так же, только доставляет ошибку.

1  void _asyncCompleteWithValue(T value) {
2    _setPendingComplete();
3    _zone.scheduleMicrotask(() {
4      _completeWithValue(value);
5    });
6  }
  • _setPendingComplete() помечает Future как завершённый.

  • _zone.scheduleMicrotask планирует выполнение _completeWithValue в следующем цикле событий.

  • _completeWithValue доставляет значение всем подписчикам (например, через then).

Future в Dart всегда доставляет свои результаты асинхронно, даже если значение уже доступно. Это сделано таким образом потому, что ожидающий Future код всегда должен работать в асинхронном контексте, независимо от того, был ли результат вычислен синхронно или асинхронно.

Использование микротасок гарантирует, что результат Future будет доставлен в следующей итерации Event Loop. Это может показаться избыточным: если результат вычисляется синхронно, почему бы просто не вернуть завершённую Future с известным результатом? Причина проста: для соблюдения асинхронной семантики Future и обеспечения согласованности в асинхронном коде.

Если бы результат доставлялся синхронно, это могло бы привести к неожиданному поведению, например:

  • нарушению порядка выполнения микротасок;

  • неправильной работе цепочек then, catchError и whenComplete, которые ожидают асинхронного выполнения.

Если бы результат Future доставлялся синхронно, то это нарушило бы семантику асинхронного API и вывод был бы следующим:

1Синхронное вычисление функции, переданной в конструктор
2Получение асинхронного результата: 1
3Выполнение после синхронной Future

Правильный вывод вы можете проверить, запустив код в DartPad.

Синхронный конструктор полезен в случаях, когда нужно гарантировать, что функция будет выполнена сразу (синхронно), но при этом результат должен быть обёрнут в Future. Это может быть необходимо для совместимости с API, которые ожидают Future, или для упрощения кода, где требуется единообразная обработка синхронных и асинхронных операций.

Доставка результата Future

Разобрав конструкторы Future, мы узнали, что результат всегда возвращается асинхронно, но не во всех случаях он будет доставлен через Event Queue. Есть исключения, когда результат доставляется через Microtask Queue. Также есть возможность использовать функцию .then() для создания цепочек вызовов, и здесь существует два пути исполнения: синхронный и асинхронный.

В DartPad собраны комбинации всех возможных запусков Future и залогированы события создания таймеров и планирования микротасок. Вы можете наглядно убедиться в том, в каких случаях создаётся микротаска, а в каких Future:

Запомните

Запомните

  1. Future — это абстракция над двумя механизмами запуска асинхронных операций scheduleMicrotask и Timer. Именно эти две абстракции реализуют асинхронный механизм в Dart.

  2. Microtask используется внутри реализации Future для доставки результата и гарантирования правильной, асинхронной, последовательности событий. Мы рассмотрели простой случай с синхронным конструктором, но есть и другие места, где внутри Future принимается решение доставить результат в следующем цикле Event Loop, но до начала выполнения следующих Future.

  3. Во время создания Future и до передачи управления DartVM происходит обращение к Zone для специальной обработки ошибки и захвата контекста исполнения.

Локальный контекст исполнения Future

Контекст исполнения — это довольно широкое понятие в программировании, которое описывает окружение исполнения кода. Оно включает в себя все состояния переменных, области вызовов, стек вызовов и состояние программы. В Dart контекст исполнения Future задают зоны (Zone). Мы уже упоминали их выше, описывая процесс создания Future.

В этом разделе мы сфокусируемся не на глобальном контексте, за который отвечает Zone, а на локальном контексте асинхронной функции. Мы опишем вызовы асинхронных операций в этой функции, а позже расширим её до класса, который может хранить состояние окружения, в котором она была вызвана.

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

Схожая проблема есть и в многопоточной парадигме программирования, так как во время работы одного потока в состояние программы может вмешаться другой, изменив контекст. Однако это скорее исключительная ситуация.

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

Для решения подобной задачи подойдёт следующий код:

1  Future<Data> fetchData() async {
2    final currentLocation = await _locationProvider.lastLocation;
3    final data = await _networkManager.executeRequest(currentLocation);
4    return data;
5  }

Метод fetchData вызывает две асинхронные функции, а async/await-синтаксис делает код удобным и лаконичным. В отличие от многопоточного программирования здесь не нужно беспокоиться о синхронизации потоков или о том, на каком потоке будут выполняться операции. Однако это не означает, что можно игнорировать внутреннюю логику асинхронных функций. Важно учитывать, сколько времени может занять их выполнение, как часто может вызываться fetchData и какие последствия это может иметь.

Например, если fetchData вызывается периодически с интервалом в 30 секунд, а получение локации занимает от 5 секунд до 20 минут, то в худшем случае может накопиться большое количество незавершённых Future.

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

Таким образом, при работе с асинхронным кодом важно учитывать не только его удобство, но и потенциальные проблемы, такие как накопление Future, долгое выполнение операций и выполнение устаревших запросов.

Важно

Важно

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

Давайте немного усложним пример и добавим условие, что локацию мы хотим получить один раз и в дальнейшем использовать только её для будущих запросов.

1Location? _caсhedLocation;
2
3Future<Data> fetchData() async {
4  final currentLocation = _caсhedLocation ??= await _locationProvider.lastLocation;
5  final data = await _networkManager.executeRequest(currentLocation);
6  return data;
7}

Этот код также, на первый взгляд, простой. Проверяем закешированную локацию. Если её нет — запускаем асинхронную операцию по её получению, записываем в переменную, идём выполнять запрос. Последующие вызовы будут использовать локацию из переменной.

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

Чтобы исправить ситуацию, в данном случае нам нужно закешировать не сам результат, а Future<Location>. При первом вызове создаётся Future, и все последующие вызовы используют этот же Future.

1Future<Location>? _cachedLocationFuture;
2
3Future<Data> fetchData() async {
4  _cachedLocationFuture ??= _locationProvider.lastLocation;
5  final currentLocation = await _cachedLocationFuture;
6  final data = await _networkManager.executeRequest(currentLocation);
7  return data;
8}

Теперь у нас будет однократный вызов, но если получения локации не произойдёт никогда, то все вызовы fetchData() зависнут навсегда, потому что Future невозможно отменить. Выполнение сетевых вызовов тоже может идти с разной скоростью, так как они выполняются конкурентно с помощью I/O Task Runner с использованием ThreadPool.

Таким образом Dart остаётся однопоточным, но выполняет сетевой запрос конкурентно, за счёт потоков TaskRunner. Если важна последовательность отправки данных, то об этом нужно отдельно задумываться и реализовывать очередь: например, гарантировать однократность вызова функции, пока не будет завершён запрос с помощью асинхронных инструментов, о которых мы поговорим позже.

Запомните

Запомните

Управление контекстом исполнения. В Dart контекст исполнения Future определяется с помощью Zone, но гораздо важнее понимание локального контекста асинхронной функции, который играет важную роль в управлении состоянием. Время исполнения вызываемых асинхронных функций, состояние переменных до и после ожидания выполнения асинхронных определяет корректность работы программы в будущем.

Асинхронно, но конкурентно. Поскольку асинхронные операции могут исполняться за пределами Dart, в нативном коде или на специальных TaskRunner, то Future исполняются в разной последовательности, что приводит к гонкам (англ. Race Condition) в коде при работе с переменными. Поэтому важно помнить про возможность накопления незавершённых Future, долгое выполнение операций, выполнение устаревших операций и невозможность отмены Future.

Отмена Future

Понятие отмены выполнения какой-либо операции довольно сложное, так как зависит от того, в каком состоянии находится операция. Если она одна и ещё не начала исполняться, то отменить такую операцию легко, удалив её из очереди.

Но если операция уже начала исполняться и в процессе исполнения находится код, результат которого мы ожидаем, то такую операцию можно прервать, лишь уничтожив весь поток исполнения. Но такое возможно только в многопоточном программировании. В асинхронном программировании у нас один поток, и его уничтожение приведёт к завершению работы программы.

Ситуацию осложняет тот факт, что в реальности редко существует одна асинхронная функция, которую ожидает другая. Часто это большие цепочки ожидающих друг друга функций, и встаёт вопрос: что делать другим функциям при отмене операции? Получается, нам нужна не просто отмена Future, а отмена с прерыванием исполнения.

Future — это обещание вернуть результат, а прерывание вычисления этого результата означает, что возвращать будет нечего. Функцию, которая пообещала Future<void>, прервать и правда легко. Но вот что делать с тем, кто пообещал Future<Data>? Остаётся только вернуть ошибку. И именно так поступают во множестве случаев и других языках программирования, где существует асинхронная модель и есть возможность отмены.

Интересно, что у Timer есть метод cancel(), который позволяет отменить выполнение кода, переданного в callback(). А ведь если Future использует Timer для исполнения кода, почему у Future нет такого же метода?

Дело в том, что Timer — это механизм, который откладывает выполнение кода и позволяет управлять этим процессом. Пока таймер находится в состоянии pending, его можно отменить вызовом cancel(), и запланированный код просто не будет выполнен.

Future, наоборот, представляет собой обещание, что когда-то в будущем станет доступен результат. После того как Future создан, он неизбежно либо завершится успешно, либо завершится с ошибкой.

Важно

Важно

Timer управляет выполнением кода, а Future только хранит результат.

К тому же отмена Timer возможна, только пока он находится в состоянии pending. Если взглянуть на пример из документации, то можно увидеть, что таймер, запланированный на 5 секунд, отменяется синхронным кодом. По истечении 5 секунд отменить выполнение функции нельзя.

1final timer = Timer(const Duration(seconds: 5), () => print('Timer finished'));
2// Cancel timer, callback never called.
3timer.cancel();

Теперь вспомним, что простой конструктор Future(() => print('No chance to be cancelled')); создаётся, как таймер с нулевой задержкой, то есть аналог этого кода без Future выглядит следующим образом:

1final timer = Timer(Duration.zero, () => print('One chance to be cancelled'));
2// Cancel timer, callback never called.
3timer.cancel();

Данный таймер запланирован уже на следующую итерацию Event Loop, которая начнётся сразу же, как закончится выполнение синхронного кода и цикла микротасок. В этом примере отмена сработает, но если перед вызовом timer.cancel() появится await, то отменить этот таймер уже нельзя.

Если мы вернёмся к реализации конструктора Future, станет наглядно понятно, что отмена вызова функции, передаваемой в таймер, приведёт к зависанию Future навсегда, ведь не будет выполнено обещание о том, что результат точно будет.

Запомните

Запомните

  1. Timer.cancel() просто удаляет задачу из очереди выполнения, а Future — это неизменяемый объект, который уже запущен и может быть выполнен не только Dart-кодом, но и в другом Isolate или в платформенном коде.

  2. Future невозможно отменить. В Dart нет возможности отменить исполнение кода, переданного в конструктор Future, так как есть обязательство вернуть результат.

  3. Обработка результата требует внимания. Мы не можем отменить асинхронную операцию, но можем отказаться от ожидания этого результата или так изменить состояние программы, что код внутри Future изменит своё поведение. Инструменты для этого мы рассмотрим в следующем параграфе.

Абстракция FutureOr

FutureOr — это одна из важнейших абстракций в языке программирования Dart, которая решает специфические проблемы при работе с асинхронным кодом. Она связующее звено между синхронным и асинхронным миром. Этот объект нельзя аллоцировать через конструктор, но его можно объявлять в сигнатурах функций.

FutureOr<T> — это абстракция, представляющая собой объединение двух типов: Future<T> и T. Другими словами, переменная типа FutureOr<T> может содержать либо значение типа T непосредственно, либо Future, который в будущем разрешится значением типа T. Это объединение типов определено таким образом, что FutureOr<Object> является одновременно супер- и подтипом Object.

Синтаксически это выглядит так: FutureOr<String> result; // может быть String или Future<String>

Зачем нужна абстракция FutureOr

FutureOr решает важную проблему в асинхронном программировании на Dart. Она позволяет создавать API, которые могут работать как с синхронными, так и с асинхронными результатами выполнения, без необходимости создавать дублирующие методы или заставлять использовать Future там, где это не требуется.

Основные преимущества:

  • Гибкость API — возможность возвращать как мгновенный результат, так и отложенный.

  • Улучшение производительности — не нужно оборачивать синхронный результат в Future.

  • Более явное указание намерений в сигнатурах методов.

Сценарии использования

Выделим три основных:

  1. Кеширование данных.

  2. Абстракции для библиотек.

  3. Тестирование.

Рассмотрим их подробнее.

Кеширование данных

Один из самых распространённых сценариев — реализация кеширования. Метод может вернуть значение немедленно (если оно в кеше) или Future (если требуется загрузка):

1FutureOr<UserData> getUserData(String userId) {
2  if (cache.containsKey(userId)) {
3    return cache[userId]; // Возвращает UserData напрямую
4  } else {
5    // Возвращает Future<UserData>
6    return fetchUserFromServer(userId).then((data) {
7      cache[userId] = data;
8      return data;
9    });
10  }
11}

Абстракции для библиотек

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

1// Метод обработки может работать с любым типом результата
2FutureOr<void> processResult(FutureOr<List<Item>> itemsResult) {
3  
4  void handle(List<Item> items) {
5    for (final item in items) {
6      print(item);
7    }
8  }
9
10  if (itemsResult is Future<List<Item>>) {
11    return itemsResult.then(handle);
12  } else {
13    handle(itemsResult);
14  }
15}

Тестирование

FutureOr особенно полезен при написании тестов, когда нужно имитировать как синхронные, так и асинхронные ответы:

При работе с FutureOr следует помнить несколько важных моментов:

  • Для работы с FutureOr чаще всего используется ключевое слово await, которое корректно обрабатывает оба варианта.

  • В функциях, возвращающих FutureOr, не нужно указывать async. Часто такое применяется при разработке API.

  • Явно указана возможность как синхронного, так и асинхронного поведения: улучшается читаемость и модульность кода.


В этом параграфе мы разобрали, что Future — это не просто синтаксический сахар, а сложная абстракция, построенная на Timer и scheduleMicrotask.

Теперь вы понимаете, как Event Loop управляет выполнением асинхронных операций, почему микротаски имеют приоритет и, главное, почему даже синхронно вычисленный результат всегда доставляется асинхронно для сохранения целостности и предсказуемости кода.

На практических примерах увидели, как важно учитывать локальный контекст асинхронной функции.

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

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

Наконец, вы познакомились с ключевой абстракцией FutureOr — элегантным мостом между синхронным и асинхронным мирами, который позволяет создавать по-настоящему гибкие и производительные API.

В следующем параграфе рассмотрим Zone в Dart. Этот механизм играет важную роль в управлении контекстом выполнения асинхронного кода и позволяет решать ряд задач, связанных с обработкой ошибок, мониторингом асинхронных операций и сохранением контекста между различными асинхронными вызовами.

Чтобы добавить в заметки выделенный текст, нажмите Ctrl + E

Отмечайте параграфы как прочитанные, чтобы видеть свой прогресс обучения

Предыдущий параграф4.7. Продвинутая навигация: в вебе и на мобильных платформах
Следующий параграф4.9. Продвинутая асинхронность: Zone