Состояния загрузки и ошибки очень часто встречаются в приложениях, работающих асинхронно. Если мы не отобразим пользовательский интерфейс (UI) загрузки или ошибки, когда это необходимо, пользователи могут посчитать, что приложение не работает, и не определят, была ли операция, которую они пытаются выполнить, успешной. Для примера, вот страница с кнопкой, которую мы можем использовать при оплате товара с помощью Stripe: Анимация:  Пример страницы оплаты с помощью Stripe Как мы видим, при нажатии кнопки "Pay (Оплатить)" появляется индикатор загрузки. И сама платежная страница также показывает индикатор загрузки, до тех пор, пока не будут доступны способы оплаты. Если платеж не проходит по какой-либо причине, надо показать пользователю UI с ошибкой, чтобы проинформировать его об этом. Давайте попробует вникнуть в суть, чтобы узнать, как можно решить эти проблемы в наших приложениях Flutter. Состояния загрузки и ошибки очень распространены, и мы должны их обрабатывать на каждой странице или виджете, который работает асинхронно. Для примера предположим, что у нас есть кнопка PaymentButton, которую можно использовать для совершения платежа: Можно сделать этот виджет стейтфул и добавить две переменные состояния: Этот подход будет работать, но он весьма рутинный и чреват ошибками. В конце концов, мы же не хотим сделать все наши виджеты стейтфул и повсюду добавлять переменные состояния, верно? Что нам действительно нужно, так это последовательный способ управления состояниями загрузки и ошибки во всем приложении. Для этого мы воспользуемся AsyncValue и StateNotifier из пакета Riverpod. Когда мы закончим, то сможем отображать любой UI загрузки и ошибки с помощью нескольких строк кода, как показано здесь: Но давайте будем действовать поэтапно. Начнем с базового виджета PaymentButton, который был представлен ранее: Когда кнопка нажата, мы вызываем ref.read(), чтобы с помощью сервиса оформления заказа совершить оплату. Если вы не знакомы с ConsumerWidget и синтаксисом ref.read(), обратитесь к моему Essential Guide to Riverpod. Для справки, вот как можно имплементировать CheckoutService и соответствующий провайдер: Это работает, но метод pay() может затянуться на несколько секунд, и у нас нет никакого UI загрузки или ошибки. Давайте разберемся с этим. В нашем примере UI должен управлять тремя возможными состояниями: не загружается (по умолчанию) загрузка ошибка Для представления этих состояний мы можем использовать класс AsyncValue, поставляемый вместе с пакетом Riverpod. Для справки, вот как определяется этот класс: Обратите внимание, что данный класс является абстрактным и что мы можем инстанцировать его только с помощью одного из существующих фабричных конструкторов. А под капотом эти конструкторы реализованы с помощью следующих конкретных классов: Самое главное, что мы можем использовать AsyncValue для представления трех состояний, которые нас так интересуют: не загружено → AsyncValue.data загрузка → AsyncValue.loading ошибка → AsyncValue.error Но где мы должны разместить нашу логику? Для этого необходимо определить подкласс StateNotifier, который будет использовать AsyncValue<void> в качестве состояния. Сначала мы определим класс PaymentButtonController, который использует CheckoutService в качестве зависимости и устанавливает состояние по умолчанию: Примечание: AsyncValue.data() обычно используется для передачи некоторых данных с помощью обобщенного аргумента <T>.  Но в нашем случае нет данных, поэтому можно использовать AsyncValue<void> для определения нашего StateNotifier и AsyncValue.data(null) при установке начального значения.  Затем добавляем метод pay(), который будет вызываться из класса виджета: Обратите внимание, как состояние устанавливается несколько раз, чтобы наш виджет мог соответствующим образом перестраивать и обновлять UI. Чтобы сделать PaymentButtonController доступным для нашего виджета, определяем StateNotifierProvider следующим образом: Обновленный виджет PaymentButton Теперь, когда у нас есть PaymentButtonController, его можно использовать в нашем классе виджета: Несколько замечаний: мы используем ref.listen() и state.whenOrNull() для показа снэкбара, если найдено состояние ошибки мы проверяем, является ли состояние платежа инстансом AsyncLoading<void> (помните: AsyncLoading является подклассом AsyncValue) мы передаем переменную isLoading в PrimaryButton, который позаботится о демонстрации правильного UI. Если вы не знакомы с листенерами в Riverpod, см. Раздел «Прослушивание изменений состояния провайдера» в моем основном руководстве по Riverpod. Это работает, но можем ли мы получить такой же результат с меньшим количеством шаблонного кода? Давайте определим расширение для AsyncValue<void>, чтобы было легче проверять состояние загрузки и показывать снэкбар при ошибке: С помощью этих изменений упрощаем наш класс виджета: При этом, для данной страницы, состояния загрузки и ошибки обрабатываются надлежащим образом: Заключение Вот законченная имплементация для расширения AsyncValueUI: Благодаря методам расширения AsyncValueUI мы можем легко обрабатывать состояния загрузки и ошибки в нашем приложении. Фактически, для каждой страницы, которая выполняется асинхронно, нам нужно сделать два шага: добавить подкласс StateNotifier<VoidAsyncValue>, являющийся посредником между классом виджета и указанными выше классами сервиса или хранилища модифицировать метод build() виджета, обрабатывая состояние ошибки через ref.listen() и проверяя состояние загрузки по мере необходимости. Хотя для того, чтобы настроить все подобным образом, потребуется немного предварительной работы, полученные преимущества оправдывают затраченное время: мы можем обрабатывать состояния загрузки и ошибки с помощью небольшого количества кода в наших виджетах; мы можем перенести всю логику управления состояниями из наших виджетов в отдельные классы контроллеров. Материал подготовлен в рамках курса «Flutter Mobile Developer». Всех желающих приглашаем на бесплатный двухдневный интенсив «Flutter engine, анимация и ее оптимизация». На этом интенсиве мы рассмотрим самые глубокие механизмы Flutter Engine и научимся создавать сложные и плавные анимации как на мобильных платформах, так и веб-версии, использовать инструменты профилирования для исключения "замерзания" интерфейса. Также мы затронем тему использования WebGL в веб-приложениях на Flutter для создания трехмерных сцен. Регистрация здесь. Переводчик, редактор