Flutter Isolates: как обработка изображений перестала вешать интерфейс
Обработка изображений на главном потоке — классическая ошибка, из-за которой приложение тормозит и раздражает пользователя. Разбираемся, как Flutter Isolates решают эту проблему за несколько строк кода.
Если приложение виснет при сжатии фото, значит, вы тяжёлую работу взвалили на тот же поток, что рисует кнопки. Flutter Isolates — способ отправить эту работу в фон, не переписывая архитектуру.
Проблема: один поток на всё
Представьте типичное мобильное приложение: пользователь выбирает фото из галереи, приложение должно его сжать, уменьшить, может быть — наложить фильтр или распознать текст через OCR. Всё это процессороёмкие операции.
Начинающие разработчики часто делают их прямо в основном потоке — том самом, который отвечает за отрисовку интерфейса и обработку касаний. Результат предсказуем: интерфейс начинает подвисать, кнопки не реагируют, а Android в логах выбрасывает что-то вроде "Skipped 120 frames! The application may be doing too much work on its main thread".
Это не баг — это архитектурная ошибка. Один поток не может одновременно рисовать виджеты и пережимать шеститысячнопиксельную фотографию.
Что такое Isolate и почему это не поток
Dart, на котором написан Flutter, изначально однопоточный язык. Но он предоставляет механизм изолятов (Isolates) — отдельных сред выполнения с собственной памятью.
Ключевое отличие от классических потоков: изоляты не разделяют память. Они общаются между собой, пересылая сообщения. Это делает их безопаснее (нет гонок данных) и проще в отладке.
Схематично это выглядит так:
- Главный изолят — рисует интерфейс, обрабатывает касания, обновляет состояние.
- Фоновый изолят — выполняет тяжёлую работу: изменение размера, сжатие, фильтры, распознавание.
Пока фоновый изолят занят, главный продолжает рисовать кадры. Пользователь видит спиннер загрузки, а не зависший экран.
compute() — самый простой путь
Flutter предоставляет несколько механизмов для работы с изолятами. Самый простой — функция compute() из пакета flutter/foundation.dart. Она автоматически создаёт временный изолят, выполняет в нём переданную функцию и уничтожает его после завершения.
Выглядит это так:
import 'package:flutter/foundation.dart';
import 'package:image/image.dart' as img;
import 'dart:typed_data';
// Функция ДОЛЖНА быть верхнего уровня (top-level) или статической
Uint8List resizeImage(Uint8List bytes) {
final image = img.decodeImage(bytes)!;
final resized = img.copyResize(image, width: 1200);
return Uint8List.fromList(img.encodeJpg(resized));
}
// Вызов — одна строка
final result = await compute(resizeImage, imageBytes);
Вот и всё. Вся тяжёлая работа ушла в фон. Главный изолят свободен для отрисовки.
Важное ограничение: функция, передаваемая в compute(), должна быть объявлена на верхнем уровне файла или быть статическим методом. Методы экземпляра не работают — изоляты не имеют доступа к состоянию виджета.
Что можно и нельзя передавать
Поскольку изоляты не разделяют память, данные между ними сериализуются. Это значит, что пересылать можно только простые типы:
Uint8List,String,int,doubleListиMap(состоящие из простых типов)
Передача сложных объектов виджетов, открытых дескрипторов файлов или контроллеров не сработает. Об этом часто забывают — и потом гадают, почему данные не приходят.
Когда изоляты действительно нужны
Не каждая операция требует отдельного изолята. Создание изолята имеет небольшие накладные расходы, поэтому для мелких задач (пара миллисекунд) проще остаться в главном потоке.
Реальные сценарии, где изоляты оправданы:
- Обработка изображений — ресайз, сжатие, наложение фильтров, конвертация форматов.
- OCR и распознавание — текст, штрихкоды, QR-коды, лица.
- Генерация документов — PDF, отчёты с форматированием.
- Парсинг больших данных — JSON-файлы на сотни килобайт, CSV-экспорт.
- Криптография — шифрование и дешифрование файлов.
- Машинное обучение — инференс моделей на устройстве.
А вот сетевые запросы в изолят переносить смысла нет — HTTP-вызовы и так асинхронные и не блокируют UI.
Типичные ошибки
Обработка в build(). Метод build() может вызываться десятки раз при перестроении дерева виджетов. Если запустить сжатие изображения внутри него — приложение будет повторять тяжёлую работу снова и снова.
Отсутствие индикатора загрузки. Если обработка занимает 2–3 секунды, пользователь должен видеть спиннер или прогресс-бар. Без этого кажется, что приложение зависло.
Передача неподходящих объектов. Пытаетесь отправить в изолят экземпляр StatefulWidget? Не получится. Передавайте байты, строки и числа.
Изоляты для мелочей. Изменение переменной или простое вычисление не требуют отдельного изолята. Накладные расходы на создание превысят выгоду.
compute() vs Isolate.spawn() vs Isolate.run()
В оригинальной статье упоминаются два механизма: compute() и Isolate.spawn(). Но есть и третий — Isolate.run(), который в современных версиях Dart является предпочтительным. Как справедливо отметил в комментариях Google Developer Expert Randal Schwartz, compute() внутри себя уже вызывает Isolate.run().
Что выбрать:
compute()— простейший вариант для одноразовых задач. Бросил функцию, получил результат, забыл.Isolate.run()— современный аналог, чуть более гибкий.Isolate.spawn()— для долгоживущих фоновых задач с постоянным обменом сообщениями черезSendPortиReceivePort. Сложнее, но позволяет не создавать изолят заново каждый раз.
Для обработки изображений compute() более чем достаточен. Переходить к Isolate.spawn() стоит, если фоновый изолят должен работать постоянно — например, обрабатывать поток кадров с камеры.
Почему это важно для мобильных приложений
На слабых устройствах — а их на рынке большинство — разница между "обработка в главном потоке" и "обработка в изоляте" — это разница между "приложение зависло" и "приложение работает плавно".
Промышленные Flutter-приложения используют изоляты повсеместно: камеры с фильтрами в реальном времени, соцсети, сжимающие фото перед загрузкой, сканеры документов с OCR, редакторы изображений. Если в вашем приложении есть хотя бы одна операция, которая занимает больше сотни миллисекунд — пора смотреть в сторону изолятов.
Итого
Flutter Isolates — это не продвинутая экзотика, а рабочий инструмент, который решает конкретную проблему: тяжёлые вычисления не должны мешать интерфейсу. compute() позволяет начать использовать его за пять минут, а эффект заметен сразу — плавные анимации, отзывчивые кнопки, довольные пользователи.
Если ваше приложение тормозит при обработке фотографий — проверьте, не делаете ли вы это на главном потоке. Скорее всего, ответ — да.