Как передавать данные между изолятами (isolates) в Dart?

«Как передавать данные между изолятами (isolates) в Dart?» — вопрос из категории Dart Core, который задают на 29% собеседований Flutter Разработчик. Ниже — развёрнутый ответ с разбором ключевых моментов.

Ответ

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

1. Базовый механизм с SendPort/ReceivePort: Это основа для любого взаимодействия. Вы создаете порт в основном изоляте, передаете SendPort в порожденный изолят, и они могут обмениваться сообщениями.

Future<void> mainIsolate() async {
  // 1. Создаем порт для получения сообщений
  final receivePort = ReceivePort();

  // 2. Создаем новый изолят, передавая ему наш SendPort
  final isolate = await Isolate.spawn(
    _backgroundTask,
    receivePort.sendPort, // Передаем SendPort как начальное сообщение
  );

  // 3. Подписываемся на сообщения от фонового изолята
  receivePort.listen((message) {
    print('Main isolate received: $message');
    if (message is SendPort) {
      // Получили SendPort от фонового изолята, можем ему отвечать
      message.send('Hello from main isolate!');
    }
    // Закрываем порт и убиваем изолят, когда работа закончена
    receivePort.close();
    isolate.kill(priority: Isolate.immediate);
  });
}

// Функция, которая выполняется в новом изоляте
void _backgroundTask(SendPort mainSendPort) {
  // 1. Создаем свой порт для получения ответов
  final isolateReceivePort = ReceivePort();
  // 2. Первым делом отправляем в основной изолят свой SendPort
  mainSendPort.send(isolateReceivePort.sendPort);

  // 3. Выполняем тяжелую задачу (например, парсинг JSON)
  final result = _performHeavyComputation();

  // 4. Отправляем результат обратно
  mainSendPort.send(result);
}

2. Упрощенный способ с compute() (идеально для одноразовых задач): Flutter предоставляет функцию compute, которая скрывает всю работу с портами. Она запускает функцию в новом изоляте, передает ей аргумент и возвращает результат.

// Функция должна быть топ-уровневой или статической
String _parseComplexJson(String jsonString) {
  // ... тяжелые вычисления ...
  return parsedResult;
}

Future<void> parseData() async {
  final heavyJson = await _loadJsonFromAssets();
  // compute сама создаст изолят, передаст данные и вернет результат
  final parsedResult = await compute(_parseComplexJson, heavyJson);
  print(parsedResult);
}

3. Использование пакетов для более сложных сценариев: Для постоянного двухстороннего общения (например, для воркеров) я использовал пакеты вроде flutter_isolate или архитектурные решения на основе Stream/StreamController, которые оборачивают логику передачи сообщений.

Важные ограничения:

  • Передаваемые данные должны быть примитивами (числа, строки, булевы), List или Map из примитивов, или объектами с реализованными методами toJson()/fromJson(). Нельзя передать, например, открытый Socket или экземпляр несериализуемого класса.
  • Функция, передаваемая в Isolate.spawn, должна быть топ-уровневой или статической методом.