В предыдущем параграфе мы во всех нюансах и подробностях рассмотрели Future.
В этом — продолжим наш углублённый разговор про асинхронность: на очереди у нас Zone.
Мы узнаем, как Zone определяет контекст выполнения асинхронного кода в Dart, рассмотрим возможности глобальной обработки исключений и мониторинга асинхронных операций, а также разберём нюансы работы с зонами, включая их влияние на кеширование результатов асинхронных операций и обработку ошибок.
Давайте приступим.
Что такое Zone
Зона в Dart — это контекст выполнения, который сохраняется между асинхронными операциями. В Dart весь код выполняется в контексте какой-либо зоны. При старте приложения существует корневая зона Zone.root, а все остальные зоны — её форки, добавляющие специфическое поведение или контекст. Получить текущую зону можно с помощью вызова Zone.current.
Методы Zone
У Zone есть три основные группы методов:
-
run*; -
bind*; -
register*.
Каждая из них служит разным целям в управлении асинхронным кодом. Рассмотрим их подробнее.
Группа методов run*
Эта группа отвечает за непосредственное выполнение кода в текущей зоне.
-
run<R>(R action()) -
runGuarded(void action()) -
runUnary<R, T>(R action(T argument), T argument) -
runUnaryGuarded<T>(void action(T argument), T argument) -
runBinary<R, T1, T2>(R action(T1 arg1, T2 arg2), T1 arg1, T2 arg2) -
runBinaryGuarded<T1, T2>(void action(T1 arg1, T2 arg2), T1 arg1, T2 arg2)
Ключевое отличие: методы run* просто запускают переданную функцию (action) в контексте зоны. Варианты с суффиксом Guarded дополнительно оборачивают выполнение в блок try-catch, что позволяет перехватывать синхронные ошибки и передавать их обработчику ошибок зоны. Варианты Unary и Binary предназначены для выполнения функций с одним или двумя аргументами соответственно.
Группа методов register*
Эта группа используется для регистрации асинхронных колбэков. Регистрация позволяет зоне «узнать» о колбэке до его фактического вызова.
-
registerCallback<R>(R callback()) -
registerUnaryCallback<R, T>(R callback(T arg)) -
registerBinaryCallback<R, T1, T2>(R callback(T1 arg1, T2 arg2))
Ключевое отличие: в противоположность run*, методы register* не выполняют колбэк. Они лишь сообщают зоне о его существовании. Это даёт зоне возможность подготовиться к будущему запуску: например, сохранить текущий стек вызовов или обернуть колбэк в другую функцию. Этот механизм нужен для реализации кастомной логики в асинхронных операциях. Зарегистрированный колбэк позже будет выполнен с помощью одного из методов run*.
Группа методов bind*
Методы этой группы — удобная обёртка, которая объединяет регистрацию и подготовку к запуску.
-
bindCallback<R>(R callback()) -
bindUnaryCallback<R, T>(R callback(T argument)) -
bindBinaryCallback<R, T1, T2>(R callback(T1 argument1, T2 argument2))
Ключевое отличие: методы bind* представляют собой сокращение для последовательного вызова register* и run*. Они регистрируют колбэк в текущей зоне и немедленно возвращают новую функцию. При вызове этой новой функции исходный колбэк будет выполнен внутри той зоны, где был вызван bind*. Это гарантирует, что колбэк всегда будет запускаться в правильном контексте, упрощая работу с асинхронными API.
Для удобства сравнили их в таблице:
|
Группа |
Назначение |
Когда использовать |
|
|
Непосредственное выполнение функции в зоне |
Когда нужно просто запустить код в определённом контексте |
|
|
Только регистрация колбэка без его выполнения |
Когда нужно получить тонкий контроль над асинхронными операциями, например, для логирования или модификации колбэков перед их запуском |
|
|
Регистрация колбэка и возврат новой функции для его последующего запуска в той же зоне |
Когда нужно передать колбэк в другой код, но обеспечить его выполнение в исходной зоне |
Зачем нужны Zone
Это сложный инструмент, который позволяет управлять выполнением кода и сохранять данные контекста выполнения, следить за асинхронными операциями, оправляемыми в Event Loop, обрабатывать ошибки или вызовы print.
Основные преимущества:
-
Изоляция выполнения асинхронного кода в отдельной песочнице.
-
Обработка не отловленных исключений на разных уровнях кода.
-
Возможность переопределения методов запуска таймеров, планирования микротасок и print.
-
Возможность перехвата вызовов входа и выхода из конкретной зоны.
Сценарии использования
Выделим два основных:
-
Обработка исключений
-
Логирование и отладка
Рассмотрим их подробнее.
Обработка исключений
Когда в приложении возникает исключение, которое не было обработано в блоке try-catch, оно может привести к завершению работы приложения. Благодаря Zone создаётся глобальный обработчик таких исключений runZonedGuarded:
1void main() {
2 runZonedGuarded(() {
3 runApp(MyApp());
4 }, (error, stackTrace) {
5 print('Произошла необработанная ошибка: $error');
6 reportCrashlytics(error, stackTrace);
7 });
8}
При создании приложения мы сразу делаем форк корневой зоны и добавляем ему колбэк на обработку ошибок. Внутри исполняется весь код приложения. Мы обрабатываем ошибки асинхронных операций и бизнес-логики, кроме ошибок во фреймворке Flutter (дерево виджетов, анимации, рендеринг компонентов) и в нативном коде Android и iOS.
Начиная с Flutter 3.3 документация Flutter предлагает использовать PlatformDispatcher.instance.onError для отлова асинхронных ошибок. Этот вызов устанавливает колбэк обработки ошибок onError текущей зоне, которая в функции main является корневой.
1void main() {
2 MyBackend myBackend = MyBackend();
3 PlatformDispatcher.instance.onError = (error, stack) {
4 myBackend.sendError(error, stack);
5 return true;
6 };
7 runApp(const MyApp());
8}
Новый подход делает логику обработки ошибок более понятной. Вместо того чтобы оборачивать всё приложение в runZonedGuarded, теперь достаточно установить два отдельных обработчика для двух разных типов ошибок. Это чётко разделяет ошибки самого фреймворка и все остальные.
В ранних версиях Flutter обработка ошибок предлагалась только через создание новой зоны с помощью runZonedGuarded. Мы имеем возможность также создать зону без обработчика ошибок, используя метод runZoned, и в таком случае все возникающие ошибки будут переданы в родительскую зону. Это важное свойство, которое нужно запомнить: у зоны может быть обработчик ошибок, и это влияет на процесс передачи ошибок между зонами.
Говоря об асинхронных операторах и обработке исключений, которые не были пойманы с помощью try-catch, важно учитывать свойство, указанное в документации к методу runZonedGuarded:
The zone will always be an error-zone ([Zone.error Zone]), so returning a future created inside the zone, and waiting for it outside of the zone, will risk the future not being seen to complete.
У этой зоны всегда будет обработчик ошибок ([Zone.error Zone]), поэтому возврат
Future, созданного внутри зоны, и ожидание его результата за пределами зоны могут привести к тому, чтоFutureне будет завершен.
Это важнейшее свойство зон, которое приводит к одним из самых сложных ошибок в отладке. Ошибки никогда не пересекают границы зон обработки ошибок. Ошибки, которые пытаются пересечь границы зон обработки ошибок, считаются необработанными в исходной зоне обработки ошибок. Если во время исполнения асинхронной функции возникает ошибка, Future проверяет наличие обработки ошибок у зоны, в которой Future был создан, и доставляет ошибку только слушателям из этой зоны.
Разберём пример такого поведения. У нас есть сетевой запрос, ответ которого мы хотели бы кешировать. Кеширование решает проблему дублирования запросов. Например: появился новый слушатель, когда мы уже отправили запрос; благодаря кешу он получит ответ этого запроса, а не создаст новый.
Функция fetchData() сохраняет свой результат Future в переменную, пока не завершится выполнение Future, но в данном случае всегда получает ошибку через 2 секунды.
Создаётся первая зона. Она инициирует запрос и кеширует результат Future. Поскольку мы не дожидаемся результата, код идёт дальше, создаётся вторая зона, в которой мы повторно инициируем получение данных. Через 2 секунды Future завершится ошибкой, и сообщение об ошибке получит только первая зона. А await fetchData() останется висеть навсегда. Вывод программы получится следующий:
1main start
2start fetch data 1 zone
3start fetch data 2 zone
4main end
51 zone exception: Exception: Network Error
Такое поведение будет и при вложенных друг в друга зонах. Это важнейшее свойство зон нужно учитывать при проектировании библиотек, которые кешируют результат Future, а вызывающая логика находится снаружи библиотеки. Ваш код может не создавать зоны, но может быть вызван из разных зон. Таким образом, если вам нужно закешировать результат работы асинхронных функций Future, лучше использовать паттерн Result и не возвращать исключения для кеширующих функций.
Таким образом, при кешировании результатов работы асинхронных функций Future рекомендуется применять паттерн Result и избегать возврата исключений для кеширующих функций.
Важно
Если ожидание результата Future происходит из разных Zone, у которых установлен обработчик ошибок, то результат получит только та Zone, которая создала Future.
Логирование и отладка
Поскольку зона является контекстом исполнения, она перехватывает все вызовы создания асинхронных операций и микротасок, которые происходят внутри Zone, а также метода print() и не только.
Чтобы переопределить методы создания таймеров, микротасок и логирования, при создании зоны нужно передать объект ZoneSpecification. В этом разделе нас интересуют асинхронные операции. Изучим, как зона может помочь нам отслеживать асинхронные операции, которые выполняются внутри приложения.
Рассмотрим пример, как мы можем поймать все события создания Future и микротасок.
1 runZonedGuarded<Future<void>>(
2 runApp(MyApp()),
3 ErrorHandler.recordError,
4 zoneSpecification: ZoneSpecification(
5 scheduleMicrotask: (self, parent, zone, microtask) {
6 parent.scheduleMicrotask(
7 zone,
8 microtask,
9 );
10 },
11 createTimer: (self, parent, zone, duration, future) {
12 return parent.createTimer(zone, duration, future);
13 },
14 createPeriodicTimer: (self, parent, zone, period, future) {
15 return parent.createPeriodicTimer(zone, period, future);
16 },
17 ),
18 );
19 }
Аргументы createTimer и createPeriodicTimer — это создания непосредственно самих Future, а метод scheduleMicrotask вызывается, когда планируется выполнение очередной микротаски. Эти методы полезны тем, что, обернув соответствующими функциями, мы можем контролировать время выполнения всех операций и логировать предупреждения.
1parent.scheduleMicrotask(zone, () {
2 final start = stopwatch.elapsedMilliseconds;
3 microtask();
4 final duration = stopwatch.elapsedMilliseconds - start;
5
6 if (duration > 16) {
7 print('Warning! Microtask execution time $duration ms');
8 }
9});
Функция scheduleMicrotask получает аргументом CreateTimerHandler. Он имеет следующую сигнатуру:
1typedef CreateTimerHandler = Timer Function(Zone self, ZoneDelegate parent, Zone zone, Duration duration, void Function() f);
А теперь подробнее:
-
Zone self— это текущая зона, в которой выполняется код. Является ссылкой на саму зону, которая вызывает данный обработчик. Используется для определения текущего контекста выполнения. Может быть полезно, если нужно получить данные или свойства текущей зоны. -
ZoneDelegate parent— делегат родительской зоны, позволяет передать управление родительской зоне для выполнения стандартного поведения (например, создания таймера). -
Zone zone— зона, в которой должен быть создан таймер. Указывает зону, к которой будет привязан создаваемый таймер. Это может быть либо текущая зона (self), либо другая. Вы можете использовать этот аргумент для создания таймера в определённой зоне. -
Duration duration— длительность таймера. Указывает время, через которое должна быть вызвана колбэк-функцияf. -
void Function() f— колбэк-функция, которая должна быть выполнена после истечения времени таймера. Та самая, которую мы передали в конструктор любогоFuture. Вы можете модифицировать или обернуть эту функцию перед передачей её в родительскую зону.
Данные аргументы схожи у всех обработчиков зоны, отвечающих за создание асинхронных операций.
Запомните
Запомните
-
Zoneопределяет контекст выполнения асинхронного кода и позволяет сохранять данные контекста между различными асинхронными вызовами, что упрощает управление асинхронными операциями. -
Группы методов
run*,register*иbind*предоставляют различные возможности для управления выполнением кода в зоне: непосредственное выполнение, регистрацию колбэков без их выполнения и объединение регистрации с подготовкой к запуску соответственно. -
Зоны позволяют реализовывать глобальную обработку ошибок и мониторинг асинхронных операций, но важно учитывать, что ошибки не пересекают границы зон с обработчиками исключений, поэтому при кешировании результатов асинхронных функций рекомендуется использовать паттерн Result.
Вот и всё!
В этом параграфе мы узнали, что такое зоны, зачем они нужны и какие у них есть методы.
Благодаря этому инструменту вы сможете настроить глобальную обработку ошибок в проекте и следить за выполнением асинхронных операций.
В следующем параграфе мы поговорим про микротаски: что это такое, как их правильно использовать и чем чревато неправильное использование.
