Как обрабатывать ошибки во Flutter

Обработайте свои ошибки! В вашем приложении будут ошибки. Вы должны с ними справиться. Кто хочет увидеть «Красный экран смерти»? Разумеется, не ваши пользователи. (На самом деле, в производстве он серый, но все равно) Это плохо. Если произойдет сбой из-за неустранимой ошибки, ваше приложение должно принять меры к этому. Если произойдет сбой, ваше приложение должно сделать все возможное, чтобы сделать это изящно. Оно даже должно демонстрировать некоторую устойчивость и не давать слишком сильных сбоев - не терять никаких данных и т. Д. Ваше приложение также должно быть подотчетным - сообщать пользователю, что только что произошло, а не просто оставлять его на красном экране (на самом деле серый экран. !). Как и документация, обработка ошибок кажется последним, о чем мы, разработчики, думаем при разработке программного обеспечения. Это не хорошо.

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

- Экскурсия по основным библиотекам

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

Мне нравятся скриншоты. Нажмите «Заголовок» для Gists.

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

Никаких движущихся изображений, никаких социальных сетей

В этой статье будут файлы gif, демонстрирующие аспекты рассматриваемой темы. Однако сказано, что просмотр таких файлов gif невозможен при чтении этой статьи на таких платформах, как Instagram, Facebook и т. Д. Они могут выглядеть как статические изображения или просто пустые поля-заполнители. Помните об этом и, возможно, прочтите эту статью на medium.com.

Давай начнем.

Ловец во флаттере

Когда я начал использовать Flutter, одним из первых вопросов, которые я исследовал, было то, как он выполняет обработку ошибок, зная его важность. При этом веду меня к пакету Catcher. Его автор, Якуб Хомлала, написал замечательный пакет Dart, предлагающий разработчикам обработку ошибок в их приложениях. Он предоставил разработчикам список предопределенных подпрограмм, которые сработают, если и когда возникнет неисправимая ошибка. Кроме того, эти параметры могут различаться в зависимости от того, работает ли ваше приложение в разработке или в производстве. Это очень хороший комплект для Дартса. Конечно, это лучше, чем ничего!

Когда в ошибке

Мы еще вернемся к созданию Якуба, а пока давайте сделаем шаг назад и продемонстрируем, что происходит, когда Flutter обнаруживает ошибку. Мы будем использовать старый резервный пример, который я использовал во многих своих прошлых статьях, - стартовое приложение counter, созданное командой flutter create. В вашей любимой среде IDE при создании нового проекта Flutter вы, вероятно, будете использовать это приложение-счетчик в качестве отправной точки. Это часть того приложения, которое вы видите ниже.

Вы можете видеть, что я "выкидываю" ошибку при нажатии кнопки "+". Я знаю, что оно не очень изобретательное, но согласитесь, это такое простое приложение - здесь не с чем поработать. Кроме того, нас беспокоит не приложение; это то, что делает Flutter в ответ на эту вопиющую ошибку.

Нажатие этой кнопки инициирует перестроение. Вызывается функция setState (), что означает, что функция build () в связанном объекте State также будет вызвана вскоре после этого. Вы можете видеть, что на зеленой вставке ниже объект состояния действительно вызывается. Обратите внимание, что на большом снимке экрана вызов функции build находится в инструкции try-catch. Вы увидите операторы try-catch во всем фреймворке Flutter. Видите ли, фреймворк делает все возможное, чтобы предвидеть возможные ошибки. В этом случае, когда возникает ошибка при попытке создать виджет, эта ошибка перехватывается оператором try-catch и статической функцией ErrorWidget.builder , затем называется.

Как указано в комментариях ErrorWidget, «при возникновении ошибки при построении виджета сломанный виджет заменяется виджетом, возвращаемым этой функцией.» На двух снимках экрана ниже представлена ​​эта функция и красный экран сам-знаешь-что он возвращает. Нажмите на подпись на скриншоте, и вам будет представлен фактический код во фреймворке Flutter.

Сообщение об ошибке

Однако мы немного забегали вперед. Вернемся назад и посмотрим, что сначала передается в ErrorWidget.builder в качестве параметра. То, что передается, поступает от закрытой функции _debugReportException (). Обратите внимание, что на снимке экрана ниже есть три позиционных параметра и один именованный параметр informationCollector, переданный в функцию _debugReportException ().

Первый параметр - это функция ErrorDescription (), которая возвращает объект DiagnosticsNode, описывающий, что происходило, когда произошла ошибка (обратите внимание, что он предоставляет больше информации в режиме разработки, чем в режиме выпуска. .). Следующие два параметра - это объект Ошибка и объект StackTrace. Откровенно говоря, объектом error может быть что угодно, однако во многих случаях это либо объект AssertionError, либо объект FlutterError. Затем объект StackTrace перечисляет последовательность вызовов, приведших к ошибке, и передает все задействованные функции и объекты классов. Надеюсь, это поможет разработчикам исправить проблему.

Последний параметр, именованный параметр, называется informationCollector, и он делает именно это. Он собирает еще больше информации об ошибке. По сути, это итерационный список узлов диагностики. Это дорогостоящий процесс (требуется много циклов памяти), поэтому используется синхронный генератор sync * . Он лениво создает последовательность значений по одному в объект Iterable.

Детали

Теперь давайте заглянем внутрь функции _debugReportException (). С первого взгляда мы видим, что эта функция возвращает объект типа FlutterErrorDetails статической функции ErrorWidget.builder. Обратите внимание, что ErrorWidget.builder имеет тип:

Widget Function(FlutterErrorDetails details);

Он возвращает виджет, заменяющий тот, который не удалось построить из-за ошибки. Но опять же, я немного забегаю вперед. Вернемся к функции _debugReportException (), и мы видим, что после создания объекта исключение FlutterErrorDetails он вызывает статическую функцию reportError из класса ошибки FlutterError. См. ниже.

Сообщить об ошибке

Давайте посмотрим, что эта статическая функция reportError. Вы можете видеть, что в своих утверждениях assert он настаивает на передаче объекта FlutterErrorDetails с ненулевым исключением. Затем он проверяет, существует ли статическая функция типа FlutterExceptionHandler, и, если да, вызывает ее с объектом FlutterErrorDetails в качестве параметра. Обратите внимание, что оператор if сообщает вам, что вы можете установить для этой статической функции FlutterError.onError значение null! Это означает, что вы можете буквально игнорировать любые ошибки в своем приложении! Да, не делай этого.

Итак, для обзора, вы обнаружите, что во фреймворке Flutter статическая функция FlutterError.onError вызывается непосредственно перед статической функцией ErrorWidget.builder с оба используют один и тот же объект исключения, FlutterErrorDetails. Опять же, довольно последовательно во всем фреймворке.

Сбросить ошибку

Опять же, по замыслу статическая функция FlutterError.onError вызывается всякий раз, когда фреймворк Flutter обнаруживает ошибку. Его поведение по умолчанию - затем вызвать еще одну статическую функцию, dumpErrorToConsole. А теперь угадайте, что это значит.

Однако главное, что с этим можно сделать, это то, что именно здесь вы можете установить свою собственную функцию и переопределить это поведение по умолчанию. Видишь, куда я иду? Вы можете определить свою собственную «обработку ошибок», назначив другую функцию «voidCallback» статической функции Flutter.onError типа:

void Function(FlutterErrorDetails details)

Обратите внимание: сказано, что если сам обработчик ошибок генерирует исключение, он не будет перехвачен фреймворком Flutter. В обработчике ошибок есть средство отлова ошибок. Мы скоро об этом поговорим. На данный момент ниже вы можете видеть, что во фреймворке Flutter обработчик ошибок Flutter действительно назначен по умолчанию для «дампа» ошибок в консоли вашей IDE в процессе разработки и в файлах журнала вашего телефона в процессе разработки.

Избавьтесь от первого, а не от остальных

Мы быстро рассмотрим эту статическую функцию dumpErrorToConsole. Обратите внимание: со своей статической переменной _errorCount он подробно записывает в консоль только "первую" ошибку. Любые дальнейшие ошибки обычно сводятся только к одной строке.

Консоль знает

Таким образом, при нажатии кнопки «+» в журналах телефона записывается следующая информация, которая отображается на консоли IDE. Он отображает сообщение об исключении, однако он также дает вам ссылку на возможное местоположение ошибки, а также трассировку стека. Все для того, чтобы исправить проблему в процессе разработки.

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

Если исключение не обнаружено,« изолятор , вызвавший исключение, приостанавливается, и обычно изолятор и его программа завершаются». - Исключения

Чтобы поймать ошибку

Теперь давайте пощечину Catcher Якуба и посмотрим, что произойдет, когда мы снова нажмем кнопку + в нашем простом примере приложения. Его readme на pub.dev хорошо описывает, как внедрить Catcher в ваше приложение. На скриншотах ниже показано, как это сделать, а также что происходит, когда наконец нажимается кнопка +.

За кулисами подробный отчет собирается в переменной экземпляра: List ‹Report› _cachedReports. Catcher также представляет собой красивое диалоговое окно и «не красный» экран. На экране опечатка, но ничего страшного. Насколько мне известно, английский - не первый язык Якуба. Полагаю, он из Польши. Кроме того, он ввел локализацию в Catcher, так что вы все равно будете добавлять свои собственные сообщения.

Да, Catcher представляет вам красивый «не красный» экран. Однако Якуб сделал это отдельной опцией, чтобы вы могли продолжать использовать вместо нее «красный экран». Думаю, я знаю, почему он это сделал. Видите ли, он знает, что когда происходит сбой, ваше приложение, вероятно, теперь находится в «нестабильном состоянии». Кто знает, отображение такого красивого экрана с использованием стандартных виджетов Flutter могло вызвать каскад других ошибок. Видите ли, стандартные виджеты не используются, когда статическая функция ErrorWidget.build возвращает этот красный экран. Это я тоже объясню позже.

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

Во-первых, давайте посмотрим, что было сделано для «замены» обработки ошибок по умолчанию. Беглый взгляд на суть пакета Catcher, и вы увидите, как вводится настраиваемая обработка ошибок. Это делается в функции _setupErrorHooks (). Catcher определяет, что происходит, когда возникает ошибка во фреймворке Flutter, что происходит, если есть ошибка в функции ввода main (), и даже что происходит, если есть ошибка в самом «обработчике ошибок».

Первое, что мы видим, - это старая статическая функция Flutter.onError. Ему назначается новая процедура, вызывающая частную функцию _reportError (). Сразу после этого у вас есть собственный обработчик ошибок, который будет перехватывать любые ошибки, которые могут возникнуть в самой среде Flutter.

Затем вызывается команда Isolate.current.addErrorListener () для обнаружения любых ошибок, которые могут возникнуть в функции ввода main (). - в «корневой зоне», откуда запускаются все программы Dart. В то время как последняя функция, runZoned (), используется для обнаружения любых ошибок в коде Dart, запущенном «вне» фреймворка Flutter - например, в коде Dart внутри самого обработчика ошибок. .

Ошибки сборки виджета

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

Вы также можете увидеть, что Catcher затем принимает само приложение Flutter в качестве параметра, чтобы назначить его своему собственному отдельному потоку памяти (Isolate), а также настроить собственную обработку ошибок. Дополнительные параметры определяют, какая обработка ошибок выполняется во время разработки, а что - во время производства. Очень хорошо.

Итак, когда мы заглянем внутрь функции addDefaultErrorWidget, мы увидим, что Catcher просто назначает соответствующую функцию статической функции ErrorWidget.builder, чтобы вернуть пользовательский виджет, если сборка не удалась.

Android Studio также обрабатывает ошибки

Например, при тестировании обработчика ошибок Catcher вы можете недоумевать, почему он сначала не работает. Если вы используете Android Studio, ваша IDE может выявлять ошибки. Обязательно перейдите в Настройки ›Flutter и снимите флажок« Показывать структурированные ошибки для проблем с фреймворком Flutter », прежде чем приступить к обработке ошибок. Лично мне потребовались часы, чтобы понять, в чем проблема.

Ошибка по Щепке

Давайте рассмотрим еще одну распространенную ситуацию, когда могут возникать ошибки. Это когда вы создаете ListView с прокруткой - популярную функцию во всех мобильных приложениях. В руководстве Напишите свое первое приложение Flutter есть прекрасный пример - приложение Генератор имени при запуске. Его исходный код находится на Github. Конечно, вы можете просто нажать на подпись на скриншоте ниже, чтобы получить копию.

Объект SliverChildBuilderDelegate вызывается для построения дочерних элементов для листинга (см. Ниже). Каждый указатель ссылается на отдельный элемент, который должен быть указан в области просмотра (видимая часть списка). Обратите внимание: в этом примере приложения я обнаружил ошибку. Он должен срабатывать при запуске. Однако, поскольку создание этого списка заключено в оператор try-catch, ошибка перехватывается, и вместо этого создается виджет из частной функции _createErrorWidget () . Вы можете видеть, что этой частной функции явно передаются объект Exception и объект StackTrace.

Внутри функции _createErrorWidget () вы видите, что оба наших «обработчика ошибок» вызываются один за другим (см. Снимки экрана ниже). Обоим предоставляется один и тот же объект FlutterErrorDetails с описанием контекста «building». Довольно просто. Итак, это структура, присутствующая во всем фреймворке Flutter, и, следовательно, последовательность событий в случае возникновения ошибки:

Вызывается операция, подверженная ошибкам.
Она находится внутри оператора try-catch.
Ошибка вызывает функцию в предложении catch. .
Создается объект FlutterErrorDetails.
Вызывается FlutterError.reportError ().
Вызывается FlutterExceptionHandler, onError.
Наконец, вызывается ErrorWidget.builder () .

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

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

В Поваренной книге Flutter.dev есть разделы, описывающие некоторые из рассмотренных нами сегодня способов обработки ошибок. Я предоставил здесь ссылки для дальнейшего использования: Создать функцию для сообщения об ошибках и Выявление и сообщение об ошибках Dart.

TL;DR

Если вы читали мои статьи раньше, то знаете, что мне нравятся варианты. Какой разработчик не любит варианты ?! Итак, конечно, вы знаете, что я сделал. Я написал свой собственный обработчик ошибок. Один с вариантами. Более подробно это описано в дополнительной статье Обработчик ошибок для Flutter.

Если мы вернемся к нашему первому примеру, старому приложению Counter, и закомментируем Catcher, чтобы заменить его моим маленьким классом ErrorHandler. Когда мы снова нажмем кнопку + и вызовем ошибку, мы получим следующее:

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

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

Catcher обеспечивает немного больше «ручного управления» и предоставляет заранее определенные функции. Однако в моем классе все зависит от вас. Вы составляете свои собственные процедуры обработки ошибок. Кроме того, в отличие от Catcher, вам придется явно включать локализацию и тому подобное в свои процедуры.

Ниже приведен снимок экрана класса ErrorHandler. Обратите внимание, как конструктор сохраняет «текущий» обработчик исключений и построитель ErrorWidget, а затем устанавливает любые процедуры, переданные в качестве параметров. Обратите внимание: это обычный класс, а не статический фабричный класс, позволяющий создавать более одного экземпляра ErrorHandler. Итак, зачем вам создавать более одного этого класса?

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

Авария в аварии

Но для чего все это нужно? Хорошо, я продемонстрирую. Вернемся к примеру приложения Startup Name Generator, давайте добавим Catcher и вызовем ту же ошибку при запуске. На приведенных ниже снимках экрана показано, что Cather использует «виджет ошибок по умолчанию» и его результат. Обратите внимание, когда происходит сбой, фон черный. Это потому, что в Catcher возникла ошибка при попытке отобразить экран с ошибкой. Видите ли, приложение находится в таком состоянии, что дополнительные виджеты вызвали дополнительные ошибки. Что ж, давай попробуем что-нибудь еще. Мы оставим все в покое, а теперь представим и этот класс ErrorHandler.

Давайте использовать как Catcher, так и класс ErrorHandler. Внутри класса RandomWordState мы видим, что теперь у него есть собственный обработчик ошибок, который создается при создании и очищается, когда сам объект State удаляется. Теперь мы снова запустим это приложение и вызовем ошибку. Смотрите, что происходит сейчас? У нас есть диалоговое окно Catcher и экран ошибок ErrorHandler. Возможно, вы могли бы назначить каждому объекту State в вашем приложении свой собственный обработчик ошибок, если вы хотите отлавливать определенные ошибки определенным образом в зависимости от того, в какой части приложения произошла ошибка. Затем вы можете сохранить некоторые данные и закрыть некоторые файлы нижнего уровня и т. Д. Обработку ошибок, специфичную для этого объекта State. Во всяком случае, это вариант.

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

Dialog Report Mode
- показывает диалог с информацией об ошибке.
Page Report Mode
- показывает новую страницу с информацией об ошибке. < br /> Обработчик консоли
- используется по умолчанию и показывает журнал сбоев в консоли.
Обработчик электронной почты вручную
- используется для пользователь для отправки электронной почты вручную.
Автообработчик электронной почты
- используется для автоматической отправки электронной почты с отчетами об ошибках.
Http Handler
- для отправки отчета об ошибке на внешний сервер.
File Handler
- для сохранения журналов в файле.
Toast Handler
-показать короткое сообщение на всплывающем экране.
Sentry Handler
- для отправки обработанных ошибок на платформу мониторинга приложений Sentry.io.
Slack Handler
- для отправки сообщений в рабочее пространство Slack.
Discord Handler
- для отправки сообщений в рабочее пространство Discord.

В любом случае, класс ErrorHandler использует те же классы, что и подпрограмма ErrorWidget.builder по умолчанию, чтобы передать сообщение об ошибке по умолчанию. Опять же, вы можете назначить свою собственную процедуру «ErrorWidget.builder», если хотите.

В любом случае! Этот класс ваш, чтобы делать то, что вам нравится. Возьми это. Сделайте его своим и, возможно, поделитесь любыми изменениями, которые вы вносите, или вообще не используйте его. Используйте Catcher! Используйте что-нибудь! Эти ошибки грядут, и вы это знаете. Будьте активны в этом отношении. Ошибки - это плохо!

Ваше здоровье.

* Исходный код по состоянию на 12 января 2020 г.

→ Другие рассказы Грега Перри