Один из самых базовых API-интерфейсов Dart для асинхронного программирования - это фьючерсы - объекты типа Future. По большей части фьючерсы Dart очень похожи на API future или обещания на других языках.

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

Благодаря функциям языка Dart, таким как async-await, вам, возможно, никогда не понадобится напрямую использовать Future API. Но вы почти наверняка встретите фьючерсы в своем коде Dart. И вы можете захотеть создать фьючерсы или прочитать код, использующий Future API.

Это вторая статья, основанная на серии видео Flutter in Focus Асинхронное программирование в Dart. В первой статье Изоляты и циклы событий были рассмотрены основы поддержки фоновой работы Dart.

Если вы предпочитаете учиться, наблюдая или слушая, все в этой статье рассматривается в следующем видео .

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

Таким образом, будущее может находиться в одном из трех состояний:

  1. Незавершенный: подарочная коробка закрыта .
  2. Завершено со значением: Коробка открыта, и ваш подарок (данные) готов.
  3. Завершено с ошибкой. Поле открыто, но что-то пошло не так.

Большая часть кода, который вы собираетесь увидеть, вращается вокруг этих трех состояний. Вы получаете будущее, и вам нужно решить, что делать, пока окно не откроется, что делать, когда оно откроется со значением, и что делать, если возникла ошибка. Вы часто будете видеть этот паттерн 1-2-3.

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

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

Допустим, у вас есть код для кнопки загрузки (реализован ниже как RaisedButton). Пользователь нажимает, и ваша кнопка начинает загрузку изображения.

Сначала происходит событие касания. Цикл событий получает событие и вызывает обработчик касания (который вы устанавливаете с помощью параметра onPressed для конструктора RaisedButton). Ваш обработчик использует библиотеку http для выполнения запроса (http.get()), а взамен получает будущее (myFuture).

Итак, теперь у вас есть маленькая коробочка, myFuture. Он начинается закрытым. Чтобы зарегистрировать обратный вызов, когда он открывается, вы используете then().

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

В конце концов, данные для изображения загружаются, и библиотека http сообщает: «Отлично! У меня это будущее прямо здесь ». Он помещает данные в поле и открывает его, что запускает ваш обратный вызов.

Теперь этот небольшой фрагмент кода, который вы передали then(), выполняется и отображает изображение.

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

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

Давайте подробнее рассмотрим Future API, некоторые из которых вы только что видели.

Хорошо, первый вопрос: как получить экземпляр Future? В большинстве случаев вы не создаете фьючерсы напрямую. Это потому, что для многих распространенных задач асинхронного программирования уже есть библиотеки, которые генерируют для вас фьючерсы.

Например, сетевое общение возвращает будущее:

final myFuture = http.get('http://example.com');

Получение доступа к общим предпочтениям также возвращает будущее:

final myFuture = SharedPreferences.getInstance();

Но вы также можете использовать Future конструкторы для создания фьючерсов.

Будущие конструкторы

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

Давайте добавим пару операторов печати, чтобы прояснить асинхронную часть:

Если вы запустите этот код в DartPad (dartpad.dev), вся основная функция завершится раньше, чем функция, переданная конструктору Future(). Это потому, что конструктор Future() сначала просто возвращает незавершенное будущее. Там написано: «Вот эта коробка. Пока держитесь за это, а позже я запущу вашу функцию и добавлю туда некоторые данные ". Вот результат выполнения предыдущего кода:

Done with main().
Creating the future.

Другой конструктор, Future.value(), удобен, когда вы уже знаете значение на будущее. Этот конструктор полезен при создании сервисов, использующих кеширование. Иногда у вас уже есть необходимое значение, поэтому вы можете вставить его прямо здесь:

final myFuture = Future.value(12);

Конструктор Future.value() имеет аналог для завершения с ошибкой. Он называется Future.error() и работает по сути так же, но принимает объект ошибки и необязательную трассировку стека:

final myFuture = Future.error(ArgumentError.notNull('input'));

Наверное, самый удобный конструктор будущего - Future.delayed(). Он работает так же, как Future(), за исключением того, что он ожидает в течение определенного периода времени перед запуском функции и завершением будущего.

Один из способов использования Future.delayed() - создание имитаций сетевых сервисов для тестирования. Если вам нужно убедиться, что ваш счетчик загрузки отображается правильно, отложенное будущее - ваш друг.

Использование фьючерсов

Теперь, когда вы знаете, откуда берутся фьючерсы, давайте поговорим о том, как их использовать. Как мы упоминали ранее, использование future-объекта в основном касается учета трех состояний, в которых оно может находиться: незавершенное, завершенное со значением или завершенное с ошибкой.

В следующем коде используется Future.delayed() для создания будущего, которое завершается через 3 секунды со значением 100.

Когда этот код выполняется, main() выполняется сверху вниз, создавая будущее и печатая «Ожидание значения…» Все это время будущее не завершено. Он не завершается еще 3 секунды.

Чтобы использовать завершенное значение, вы можете использовать then(). Это метод экземпляра для каждого future-объекта, который можно использовать для регистрации обратного вызова, когда future завершается со значением. Вы даете ему функцию, которая принимает единственный параметр, соответствующий типу будущего. Как только future завершается со значением, ваша функция вызывается с этим значением.

Вот результат выполнения предыдущего кода:

Waiting for a value... (3 seconds pass until callback executes)
The value is 100.

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

Вернемся к нашему первому примеру: что произойдет, если это начальное будущее не завершится значением - что, если оно завершится с ошибкой? Метод then() ожидает значение. Вам нужен способ зарегистрировать другой обратный вызов в случае ошибки.

Ответ - использовать catchError(). Он работает так же, как then(), за исключением того, что принимает ошибку вместо значения и выполняется, если future завершается с ошибкой. Как и then(), метод catchError() возвращает собственное будущее, поэтому вы можете построить целую цепочку из then() и catchError() методов, которые ожидают друг друга.

Примечание. Вам не нужно звонить then() или catchError(), если вы используете языковую функцию async-await. Вместо этого вы ждете завершенного значения и используете try-catch-finally для обработки ошибок. Дополнительные сведения см. В разделе поддержки асинхронности в языковом туре по Dart.

Вот пример использования catchError() для обработки случая, когда future завершается с ошибкой:

Вы даже можете дать catchError() тестовую функцию для проверки ошибки перед вызовом обратного вызова. Таким образом, у вас может быть несколько catchError() функций, каждая из которых будет проверять разные типы ошибок. Вот пример указания тестовой функции с использованием необязательного параметра test для catchError():

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

  1. Первый блок создает незавершенное будущее.
  2. Затем есть функция, которую нужно вызвать, если future завершается со значением.
  3. Тогда есть еще одна функция, которую нужно вызвать, если future завершится с ошибкой.

Есть еще один метод, который вы можете использовать: whenComplete(). Вы можете использовать его для выполнения функции, когда будущее завершено, независимо от того, имеет ли оно значение или ошибку.

Это похоже на блок finally в try-catch-finally. Есть код, который выполняется, если все идет правильно, код ошибки и код, который запускается несмотря ни на что.

Использование фьючерсов во Flutter

Вот как вы создаете фьючерсы и немного о том, как использовать их значения. А теперь давайте поговорим о том, как заставить их работать во Flutter.

Допустим, у вас есть сетевая служба, которая будет возвращать некоторые данные JSON, и вы хотите отобразить эти данные. Вы можете создать StatefulWidget, который создает будущее, проверяет завершение или ошибку, вызывает setState() и обычно обрабатывает все соединения вручную.

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

Виджет FutureBuilder работает, вызывая свою функцию построителя, которая принимает контекст и снимок текущего состояния будущего.

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

В противном случае вы можете проверить свойство hasData, чтобы увидеть, завершилось ли оно значением:

Если ни hasError, ни hasData не являются истинными, то вы знаете, что все еще ждете, и вы также можете вывести что-нибудь для этого.

Даже в коде Flutter вы можете видеть, как эти три состояния продолжают появляться: незавершенное, завершенное со значением и завершенное с ошибкой.

Резюме

В этой статье говорилось о том, что представляют собой фьючерсы и как можно использовать Future и FutureBuilder API для создания фьючерсов и использования их завершенных значений.

Если вы хотите узнать больше об использовании фьючерсов - с возможностью использования исполняемых примеров и интерактивных упражнений для проверки вашего понимания - ознакомьтесь с асинхронной кодовой таблицей Futures, async и await.

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

Большое спасибо Эндрю Брогдону, создавшему видео, на котором основана эта статья.