Фундаментальные концепции, известные конструкторы и полезные методы изящно обрабатывают асинхронные задачи

Предисловие

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

Эти контракты называются фьючерсами.

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

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

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

В этой статье я постараюсь:

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

Концептуальное понимание

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

Future предоставляет нам API-интерфейсы, необходимые для упрощения доступа к циклу обработки событий и обработки асинхронных задач. Это позволяет нам сделать следующее:

  • Инкапсулировать задачу и отправить на обработку
  • Определение текущего состояния задачи и ее завершения
  • Получить результат задачи, если она выполнена успешно
  • Получите ошибку, которая привела к сбою задачи

Чтобы отслеживать текущий статус Future, мы полагаемся на три различных состояния:

  • Незавершенное: задача все еще выполняется.
  • Завершено с данными: задача выполнена, и данные готовы.
  • Выполнено с ошибкой: Задача выполнена, но с ошибкой.

По завершении мы будем использовать then(), чтобы получить результат, или мы будем использовать catchError(), чтобы получить выброшенную ошибку. Это будет выглядеть примерно так:

var list = coffeeList
               .then((value) => (value)) // Success
               .catchError((error) => (error)); // Failure

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

Здесь следует отметить, что API-интерфейсы и методы Future также проводят близкие параллели с объектом Promise в JavaScript. Мы можем наблюдать за использованием then() и за тем, как он распаковывает данные, разрешая будущее.

Из-за этого сходства с Promise разработчикам с опытом работы с JavaScript будет легче усвоить использование и методологию Future.

Прежде чем мы начнем

Прежде чем мы исследуем наши различные концепции, давайте создадим основу для наших экспериментов.

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

Наши данные для горячего кофе будут отсюда: https://api.sampleapis.com/coffee/hot

Этот API возвращает нам массив горячих кофе, и один объект будет выглядеть так:

{
  "title":"Black",
  "description":"Black coffee is as simple as it gets with ground coffee beans steeped in hot water, served warm.",
  "ingredients":[
     "Coffee"
  ],
  "image":"https://upload.wikimedia.org/wikipedia/commons/thumb/4/45/A_small_cup_of_coffee.JPG/640px-A_small_cup_of_coffee.JPG",
  "id":1
}

Известные конструкторы

Конструкторы — это методы, которые мы используем для инициации экземпляра класса. Несколько конструкторов очень удобны при создании экземпляра Future, что дает нам некоторые ключевые преимущества.

Будущее()

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

void main() {
  Future<http.Response> coffeeData =
      http.get(Uri.parse('https://api.sampleapis.com/coffee/hot'));
}

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

coffeeData.then((value) => (value)) // Success
.catchError((error) => (error)); // Failure

Будущее.отложено()

Постановка задачи

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

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

Как смоделировать иллюзию HTTP-запроса и ответа для разработки пользовательского интерфейса с хорошо учтенными загрузкой и состоянием успеха?

Решение

Здесь мы можем использовать конструктор Future.delayed(). Этот конструктор позволяет нам создать задачу Future, которая запускается внутри своей инкапсуляции после указанного нами периода задержки.

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

void main() {
  List<Coffee> coffeeList = [
    const Coffee(
      id: 1,
      title: 'Black',
      description:
          "If you want to sound fancy, you can call black coffee by its proper name: cafe noir.",
      ingredients: ['Coffee'],
      image:
          "https://upload.wikimedia.org/wikipedia/commons/thumb/4/45/A_small_cup_of_coffee.JPG/640px-A_small_cup_of_coffee.JPG",
    )
  ];

  Future.delayed(const Duration(milliseconds: 2000), () {
   // The code below will be executed after a delay of 2 seconds
      return coffeeList;
   });
}

Как вы можете видеть из приведенного выше кода, мы использовали Future.delayed() для задержки возвращаемых фиктивных данных на две секунды. Это позволяет нам создать иллюзию запроса-ответа.

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

Будущее.ошибка()

Постановка задачи

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

Что, если бы мы захотели сымитировать сценарий, в котором запрос по какой-либо причине не выполняется?

Решение

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

void main() {  
final Future<Exception> apiErrorTest = Future.delayed(const Duration(milliseconds: 2000), () {
    // The code below will return an error after a delay of 2 seconds
    return Future.error(
      Exception('Failed to fetch data from coffee endpoint.'), // Throw
    );
  });
}

Как видно из приведенного выше кода, мы задерживаем ответ, используя Future.delayed(), но на этот раз он возвращает Future.error(), который содержит исключение. Используя это, мы можем эмулировать сбой вызова выборки API.

Полезные методы

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

затем()

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

void main() {
  Future<http.Response> coffeeData =
      http.get(Uri.parse('https://api.sampleapis.com/coffee/hot'));

  coffeeData.then((value) => (value));
}

В приведенном выше примере then() предоставляет нам список кофе, который мы получаем из вызова API.

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

Future<R> then<R>(FutureOr<R> onValue(T value), {Function? onError});

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

пойматьОшибку()

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

Здесь на сцену выходит catchError(). Подобно then(), catchError() также является функцией обратного вызова. Этот метод не работает отдельно и в сочетании с then() обеспечивает целостное решение. Но в отличие от then() , catchError() выдает ошибку асинхронной операции:

void main() {
  Future<http.Response> coffeeData =
      http.get(Uri.parse('https://api.sampleapis.com/coffee/hot'));

  coffeeData.then((value) => (value))
    .catchError((error) => (error));
}

В приведенном выше примере, если вызов API завершается неудачно, мы можем обработать ошибку с помощью блока кода catchError((error) => (error)).

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

Future<T> catchError(Function onError, {bool test(Object error)?});

Как мы видим из реализации, мы можем опционально добавить тесты в catchError(). Это позволяет нам узнать, с какой ошибкой мы сталкиваемся. Рассмотрим пример теста:

coffeeListFuture.then((value) => (coffeeList = getCoffeeList(value)))
  // Custom error catch block
  .catchError(
  (
    Object error,
    StackTrace stackTrace,
  ) {
    print(error.toString());
  },
  test: (Object error) {
      return error is HttpException;
  }
);

В приведенном выше блоке кода мы видим, что обратный вызов catchError() был добавлен с проверкой, чтобы проверить, является ли он типом исключения HTTP. Если тест возвращает логическое значение true, мы можем справиться с этим исключением на основе конкретных бизнес-требований для исключений HTTP.

Постановка задачи

Недостатком включения тестов является то, что этот блок catchError() будет ловить только те ошибки, которые проходят этот тест. Те, которые не соответствуют этому тесту, будут выброшены как неотловленные ошибки. Как мы захватываем те, которые не подходят? Что, если бы нам нужно было идентифицировать разные типы ошибок, а не только одну?

Решение

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

  coffeeListFuture.then((value) => (coffeeList = getCoffeeList(value)))
    // Custom error catch block
    .catchError(
        (
          Object error,
          StackTrace stackTrace,
        ) {
          print(error.toString());
        },
        test: (Object error) => error is CustomException)
    // Http error catch block
    .catchError(
        (
          Object error,
          StackTrace stackTrace,
        ) {
          print(error.toString());
        },
        test: (Object error) => error is HttpException)

В приведенном выше примере мы видим, что два метода catchError() привязаны к одному Future, первый catchError() перехватывает определенные нами пользовательские ошибки, а второй позволяет нам перехватывать исключения HTTPS.

coffeeListFuture.then((value) => (coffeeList = getCoffeeList(value)))
    // Http error catch block
    .catchError(
        (
          Object error,
          StackTrace stackTrace,
        ) {
          print(error.toString());
        },
        test: (Object error) => error is HttpException)
    // General error catch block
    .catchError(
        (error) => print(error)
);

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

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

когда завершено ()

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

Постановка задачи

Но что, если мы хотим, чтобы часть кода выполнялась независимо от результата этой асинхронной задачи? Что, если бы мы, предположительно, хотели вызвать метод для получения списка холодных сортов кофе независимо от того, что конечная точка не смогла предоставить нам список горячих сортов кофе?

Решение

Здесь на сцену выходит whenComplete(). Этот метод позволяет нам выполнять коды, которые будут выполняться независимо от того, завершится ли Future результатом или ошибкой.

Для тех из нас, кто имеет опыт работы с JavaScript, это действует так же, как finally() работает для Promise.

Давайте посмотрим, как это можно реализовать. Мы создадим функцию, которая возвращает Future с ответом HTTP, содержащим список кофе, на основе конечной точки hot или cold.

Future<http.Response> fetchCoffeeData({required String coffeeTemp}) async {
  const baseURL = 'https://api.sampleapis.com/';
  final coffeeResponse =
      await http.get(Uri.parse(baseURL + '/coffee/' + coffeeTemp));

  if (coffeeResponse.statusCode == 200) {
    return coffeeResponse;
  } else {
    throw Exception('Failed to fetch coffees');
  }
}

Теперь давайте создадим Future и присоединим к нему then(), catchError() и whenComplete(), чтобы продемонстрировать то, что мы обсуждали.

Future<http.Response> coffeeListFuture = fetchCoffeeData(coffeeTemp: 'hot');

  coffeeListFuture
      .then((value) => value)
      .catchError((error) => error)
      .whenComplete(() => fetchCoffeeData(coffeeTemp: 'cold'));

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

Заключение

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

Я надеюсь, что те, кто читал эту статью, теперь хорошо усвоили следующее:

  • Как Future работает в однопоточной среде и удобно нам для взаимодействия с циклом событий.
  • Как, по сути, очень похоже на Promise в JavaScript.
  • Как мы используем его для обработки асинхронных задач
  • Как мы можем имитировать загрузку API, используя Future
  • Как узнать, успешны ли асинхронные вызовы или нет
  • Как имитировать загрузку, успех и состояние ошибки вызова API с помощью конструкторов.
  • Какие примечательные методы у нас есть и зачем они нам нужны для обработки асинхронных задач через Futures.

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